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 001/878] 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 002/878] 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 003/878] 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 004/878] 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 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 005/878] 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 006/878] 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 007/878] 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 008/878] 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 009/878] 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 010/878] 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 011/878] 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 012/878] 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 013/878] 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 014/878] 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 015/878] 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 016/878] 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 017/878] 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 018/878] 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 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 019/878] 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 020/878] 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 021/878] 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 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 022/878] 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 023/878] 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 024/878] 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 025/878] 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 026/878] 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 027/878] 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 028/878] 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 029/878] 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 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 030/878] 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 031/878] 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 032/878] 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 033/878] 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 034/878] 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 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 035/878] 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 41b5dae606b834b29f218be5ab727e7985897c9d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 30 Aug 2018 16:52:12 +0200 Subject: [PATCH 036/878] 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 0e10fdb4150fbaf8f845c6273791b62af4defb45 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 13:24:49 +0300 Subject: [PATCH 037/878] 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 038/878] 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 039/878] 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 040/878] 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 041/878] 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 042/878] 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 043/878] 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 e8a52117a5f55e05579d29c773ca1b2e83cd2d76 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Sep 2018 16:54:52 +0300 Subject: [PATCH 044/878] 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 b891a858ca0934fbec5fd54b64d55d2763bf6f80 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 4 Sep 2018 12:49:13 +0300 Subject: [PATCH 045/878] 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 b2ba9c516626dbf3b87a3d0b5895b3aa84ffcc5a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 14:23:28 +0300 Subject: [PATCH 046/878] 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 047/878] 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 28cc605e320bf7ea1e0539df220b744e3baf6dda Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:36:22 +0300 Subject: [PATCH 048/878] 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 e67b8a3e1ad449a2d94c1578cd508438e0715222 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:14 +0300 Subject: [PATCH 049/878] 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 050/878] 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 349b2787cbb0ff664d784cb41ae2849a82141e5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 14:31:56 +0300 Subject: [PATCH 051/878] 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 e4a488baf1279d9d41afd6a4bbe84e9cb6c9a1b5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 16:12:28 +0300 Subject: [PATCH 052/878] 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 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 053/878] 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 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 054/878] 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 055/878] 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 f0e905f3c9993135603b74bb6de3b9b41e31e404 Mon Sep 17 00:00:00 2001 From: Dan Doyle Date: Wed, 12 Sep 2018 13:17:15 +0000 Subject: [PATCH 056/878] 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 f0f19e0c0334d51ce264d3d151b9d3e4a87a4f9a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 4 Sep 2018 13:21:02 +0200 Subject: [PATCH 057/878] 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 058/878] 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 059/878] 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 060/878] 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 061/878] 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 = ` - +
    {LEGEND_STATS.map( - statName => seriesValuesProps[statName] && + statName => + seriesValuesProps[statName] && ( + + ) )}
    + {statName} + {sort === statName && } + - {props.statName} - -
    + props.onClick(e)}> {statName} {sort === statName && }
    - + this.props.onLabelClick(e)} + />
    ) : null} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 9243f612466..29167dcc4c4 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -67,6 +67,16 @@ const FLOT_OPTIONS = { }; class Graph extends Component { + state = { + showAllTimeSeries: false, + }; + + getGraphData() { + const { data } = this.props; + + return this.state.showAllTimeSeries ? data : data.slice(0, 20); + } + componentDidMount() { this.draw(); } @@ -82,8 +92,19 @@ class Graph extends Component { } } + onShowAllTimeSeries = () => { + this.setState( + { + showAllTimeSeries: true, + }, + this.draw + ); + }; + draw() { - const { data, options: userOptions } = this.props; + const { options: userOptions } = this.props; + const data = this.getGraphData(); + const $el = $(`#${this.props.id}`); if (!data) { $el.empty(); @@ -124,8 +145,10 @@ class Graph extends Component { } render() { - const { data, height, loading } = this.props; - if (!loading && data && data.length === 0) { + const { height, loading } = this.props; + const data = this.getGraphData(); + + if (!loading && data.length === 0) { return (
    The queries returned no time series to graph.
    @@ -133,9 +156,21 @@ class Graph extends Component { ); } return ( -
    -
    - +
    + {this.props.data.length > 20 && + !this.state.showAllTimeSeries && ( +
    + + Showing only 20 time series.{' '} + {`Show all ${ + this.props.data.length + }`} +
    + )} +
    +
    + +
    ); } diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 903193d8b10..c4a7da46263 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -55,6 +55,25 @@ margin-top: 2 * $panel-margin; } + .time-series-disclaimer { + width: 300px; + margin: 10px auto; + padding: 10px 0; + border-radius: 4px; + text-align: center; + background-color: #212124; + + .disclaimer-icon { + color: $yellow; + margin-right: 5px; + } + + .show-all-time-series { + cursor: pointer; + color: $external-link-color; + } + } + .elapsed-time { position: absolute; left: 0; From 06d24df7b49ecaec30a1c07c21ed8d54f3a76bb9 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 24 Sep 2018 16:58:39 +0200 Subject: [PATCH 166/878] docs: postgres gif. --- docs/sources/guides/whats-new-in-v5-3.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md index ed5238d1b94..d8e60517ecb 100644 --- a/docs/sources/guides/whats-new-in-v5-3.md +++ b/docs/sources/guides/whats-new-in-v5-3.md @@ -40,6 +40,8 @@ Do you use Grafana alerting and have some notifications that are more important Grafana 5.3 comes with a new graphical query builder for Postgres. Bringing Postgres integration more in line with some the other datasources and making it easier for both advanced and beginners to work with timeseries in Postgres. +{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} + ## Improved OAuth support for Gitlab Grafana 5.3 now supports filtering to specific groups when using Gitlab OAuth. This is makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving access to Grafana to everyone. From b700c6b0e4d46f55c0c059dcec84278f39bb0e20 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 09:34:14 +0200 Subject: [PATCH 167/878] stackdriver: populate alignment and aggregation dropdowns based on metric type and value type --- pkg/tsdb/stackdriver/stackdriver.go | 3 + .../datasource/stackdriver/constants.ts | 268 ++++++++++++++++-- .../stackdriver/partials/query.editor.html | 14 +- .../datasource/stackdriver/query_ctrl.ts | 36 ++- 4 files changed, 283 insertions(+), 38 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 20df58f87d7..d9bc396038e 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -299,6 +299,9 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta Name: metricName, Points: points, }) + + queryRes.Meta.Set("metricKind", series.MetricKind) + queryRes.Meta.Set("valueType", series.ValueType) } queryRes.Meta.Set("resourceLabels", resourceLabels) diff --git a/public/app/plugins/datasource/stackdriver/constants.ts b/public/app/plugins/datasource/stackdriver/constants.ts index eb808d48408..f0d6706c1bd 100644 --- a/public/app/plugins/datasource/stackdriver/constants.ts +++ b/public/app/plugins/datasource/stackdriver/constants.ts @@ -1,37 +1,245 @@ +export enum MetricKind { + METRIC_KIND_UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED', + GAUGE = 'GAUGE', + DELTA = 'DELTA', + CUMULATIVE = 'CUMULATIVE', +} + +export enum ValueTypes { + VALUE_TYPE_UNSPECIFIED = 'VALUE_TYPE_UNSPECIFIED', + BOOL = 'BOOL', + INT64 = 'INT64', + DOUBLE = 'DOUBLE', + STRING = 'STRING', + DISTRIBUTION = 'DISTRIBUTION', + MONEY = 'MONEY', +} + export const alignOptions = [ - { text: 'none', value: 'ALIGN_NONE' }, - { text: 'delta', value: 'ALIGN_DELTA' }, - { text: 'rate', value: 'ALIGN_RATE' }, - { text: 'interpolate', value: 'ALIGN_INTERPOLATE' }, - { text: 'next older', value: 'ALIGN_NEXT_OLDER' }, - { text: 'min', value: 'ALIGN_MIN' }, - { text: 'max', value: 'ALIGN_MAX' }, - { text: 'mean', value: 'ALIGN_MEAN' }, - { text: 'count', value: 'ALIGN_COUNT' }, - { text: 'sum', value: 'ALIGN_SUM' }, - { text: 'stddev', value: 'ALIGN_STDDEV' }, - { text: 'count true', value: 'ALIGN_COUNT_TRUE' }, - { text: 'count false', value: 'ALIGN_COUNT_FALSE' }, - { text: 'fraction true', value: 'ALIGN_FRACTION_TRUE' }, - { text: 'percentile 99', value: 'ALIGN_PERCENTILE_99' }, - { text: 'percentile 95', value: 'ALIGN_PERCENTILE_95' }, - { text: 'percentile 50', value: 'ALIGN_PERCENTILE_50' }, - { text: 'percentile 05', value: 'ALIGN_PERCENTILE_05' }, - { text: 'percent change', value: 'ALIGN_PERCENT_CHANGE' }, + { + text: 'none', + value: 'ALIGN_NONE', + valueTypes: [ + ValueTypes.INT64, + ValueTypes.DOUBLE, + ValueTypes.MONEY, + ValueTypes.DISTRIBUTION, + ValueTypes.BOOL, + ValueTypes.STRING, + ], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], + }, + { + text: 'delta', + value: 'ALIGN_DELTA', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.CUMULATIVE, MetricKind.DELTA], + }, + { + text: 'rate', + value: 'ALIGN_RATE', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.CUMULATIVE, MetricKind.DELTA], + }, + { + text: 'interpolate', + value: 'ALIGN_INTERPOLATE', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE], + }, + { + text: 'next older', + value: 'ALIGN_NEXT_OLDER', + valueTypes: [ + ValueTypes.INT64, + ValueTypes.DOUBLE, + ValueTypes.MONEY, + ValueTypes.DISTRIBUTION, + ValueTypes.STRING, + ValueTypes.VALUE_TYPE_UNSPECIFIED, + ValueTypes.BOOL, + ], + metricKinds: [MetricKind.GAUGE], + }, + { + text: 'min', + value: 'ALIGN_MIN', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'max', + value: 'ALIGN_MAX', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'mean', + value: 'ALIGN_MEAN', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'count', + value: 'ALIGN_COUNT', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'sum', + value: 'ALIGN_SUM', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'stddev', + value: 'ALIGN_STDDEV', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'count true', + value: 'ALIGN_COUNT_TRUE', + valueTypes: [ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE], + }, + { + text: 'count false', + value: 'ALIGN_COUNT_FALSE', + valueTypes: [ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE], + }, + { + text: 'fraction true', + value: 'ALIGN_FRACTION_TRUE', + valueTypes: [ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE], + }, + { + text: 'percentile 99', + value: 'ALIGN_PERCENTILE_99', + valueTypes: [ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'percentile 95', + value: 'ALIGN_PERCENTILE_95', + valueTypes: [ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'percentile 50', + value: 'ALIGN_PERCENTILE_50', + valueTypes: [ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'percentile 05', + value: 'ALIGN_PERCENTILE_05', + valueTypes: [ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'percent change', + value: 'ALIGN_PERCENT_CHANGE', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, ]; export const aggOptions = [ - { text: 'none', value: 'REDUCE_NONE' }, - { text: 'mean', value: 'REDUCE_MEAN' }, - { text: 'min', value: 'REDUCE_MIN' }, - { text: 'max', value: 'REDUCE_MAX' }, - { text: 'sum', value: 'REDUCE_SUM' }, - { text: 'std. dev.', value: 'REDUCE_STDDEV' }, - { text: 'count', value: 'REDUCE_COUNT' }, - { text: '99th percentile', value: 'REDUCE_PERCENTILE_99' }, - { text: '95th percentile', value: 'REDUCE_PERCENTILE_95' }, - { text: '50th percentile', value: 'REDUCE_PERCENTILE_50' }, - { text: '5th percentile', value: 'REDUCE_PERCENTILE_05' }, + { + text: 'none', + value: 'REDUCE_NONE', + valueTypes: [ + ValueTypes.INT64, + ValueTypes.DOUBLE, + ValueTypes.MONEY, + ValueTypes.DISTRIBUTION, + ValueTypes.BOOL, + ValueTypes.STRING, + ], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED], + }, + { + text: 'mean', + value: 'REDUCE_MEAN', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'min', + value: 'REDUCE_MIN', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'max', + value: 'REDUCE_MAX', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'sum', + value: 'REDUCE_SUM', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'std. dev.', + value: 'REDUCE_STDDEV', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'count', + value: 'REDUCE_COUNT', + valueTypes: [ + ValueTypes.INT64, + ValueTypes.DOUBLE, + ValueTypes.MONEY, + ValueTypes.DISTRIBUTION, + ValueTypes.BOOL, + ValueTypes.STRING, + ], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'count', + value: 'REDUCE_COUNT_TRUE', + valueTypes: [ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: 'count', + value: 'REDUCE_COUNT_FALSE', + valueTypes: [ValueTypes.BOOL], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: '99th percentile', + value: 'REDUCE_PERCENTILE_99', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: '95th percentile', + value: 'REDUCE_PERCENTILE_95', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: '50th percentile', + value: 'REDUCE_PERCENTILE_50', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, + { + text: '5th percentile', + value: 'REDUCE_PERCENTILE_05', + valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION], + metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], + }, ]; export const alignmentPeriods = [ diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 5a87f981119..891dab6d93c 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -2,8 +2,8 @@
    Metric Type - +
    @@ -35,7 +35,7 @@
    -
    @@ -53,7 +53,7 @@
    -
    @@ -85,8 +85,8 @@
    Project - +
    -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    +
    Alias By diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts new file mode 100644 index 00000000000..79161e0ca8c --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -0,0 +1,73 @@ +import angular from 'angular'; +import _ from 'lodash'; +import * as options from './constants'; + +export class StackdriverAggregation { + constructor() { + return { + templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.aggregation.html', + controller: 'StackdriverAggregationCtrl', + restrict: 'E', + scope: { + target: '=', + refresh: '&', + }, + }; + } +} + +export class StackdriverAggregationCtrl { + target: any; + alignOptions: any[]; + aggOptions: any[]; + refresh: () => void; + + constructor(private $scope) { + this.aggOptions = options.aggOptions; + this.alignOptions = options.alignOptions; + $scope.alignmentPeriods = options.alignmentPeriods; + $scope.getAlignOptions = this.getAlignOptions; + $scope.getAggOptions = this.getAggOptions; + $scope.onAlignmentChange = this.onAlignmentChange; + $scope.onAggregationChange = this.onAggregationChange; + this.refresh = $scope.refresh; + } + + onAlignmentChange(newVal) { + if (newVal === 'ALIGN_NONE') { + this.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; + } + this.refresh(); + } + + onAggregationChange(newVal) { + if (newVal !== 'REDUCE_NONE') { + const newAlignmentOption = options.alignOptions.find(o => o.value !== 'ALIGN_NONE'); + this.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; + } + this.refresh(); + } + + getAlignOptions() { + return !this.target.valueType + ? options.alignOptions + : options.alignOptions.filter(i => { + return ( + i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 + ); + }); + } + + getAggOptions() { + return !this.target.metricKind + ? options.aggOptions + : options.aggOptions.filter(i => { + return ( + i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 + ); + }); + } +} + +angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); +angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index db276650e65..be98e9bf3ec 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; import appEvents from 'app/core/app_events'; -import * as options from './constants'; import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; +import './query_aggregation_ctrl'; export interface QueryMeta { rawQuery: string; @@ -55,8 +55,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: '', }; - alignOptions: any[]; - aggOptions: any[]; groupBySegments: any[]; removeSegment: any; showHelp: boolean; @@ -74,9 +72,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); - this.stackdriverConstants = options; - this.aggOptions = options.aggOptions; - this.alignOptions = options.alignOptions; this.getCurrentProject() .then(this.getMetricTypes.bind(this)) @@ -155,6 +150,9 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.metricLabels = data.results[this.target.refId].meta.metricLabels; this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; + + this.target.valueType = data.results[this.target.refId].meta.valueType; + this.target.metricKind = data.results[this.target.refId].meta.metricKind; resolve(); } catch (error) { resolve(); @@ -264,31 +262,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { } } - getAlignOptions() { - return !this.target.valueType - ? options.alignOptions - : options.alignOptions.filter(i => { - return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 - ); - }); - } - - getAggOptions() { - if (this.target.aggregation.perSeriesAligner === 'ALIGN_NONE') { - this.target.aggregation.crossSeriesReducer = options.aggOptions[0].value; - return options.aggOptions.slice(0, 1); - } - - return !this.target.metricKind - ? options.aggOptions - : options.aggOptions.filter(i => { - return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 - ); - }); - } - onDataReceived(dataList) { this.lastQueryError = null; this.lastQueryMeta = null; @@ -297,8 +270,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery); - this.target.valueType = anySeriesFromQuery.meta.valueType; - this.target.metricKind = anySeriesFromQuery.meta.metricKind; } } diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts new file mode 100644 index 00000000000..aebd2a12dd6 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -0,0 +1,81 @@ +import { StackdriverAggregationCtrl } from '../query_aggregation_ctrl'; + +describe('StackdriverAggregationCtrl', () => { + let ctrl; + describe('aggregation and alignment options', () => { + beforeEach(() => { + ctrl = createCtrlWithFakes(); + }); + describe('when new query result is returned from the server', () => { + describe('and result is double and gauge', () => { + beforeEach(async () => { + ctrl.target.valueType = 'DOUBLE'; + ctrl.target.metricKind = 'GAUGE'; + }); + + it('should populate all aggregate options except two', () => { + const result = ctrl.getAggOptions(); + expect(result.length).toBe(11); + expect(result.map(o => o.value)).toEqual( + expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) + ); + }); + + it('should populate all alignment options except two', () => { + const result = ctrl.getAlignOptions(); + console.log(result.map(o => o.value)); + expect(result.length).toBe(10); + expect(result.map(o => o.value)).toEqual( + expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) + ); + }); + }); + }); + + describe('when a user a user select ALIGN_NONE and a reducer is selected', () => { + beforeEach(async () => { + ctrl.target.aggregation.crossSeriesReducer = 'RANDOM_REDUCER'; + ctrl.onAlignmentChange('ALIGN_NONE'); + }); + it('should set REDUCE_NONE as selected aggregation', () => { + expect(ctrl.target.aggregation.crossSeriesReducer).toBe('REDUCE_NONE'); + }); + }); + + describe('when a user a user select a reducer and no alignment is selected', () => { + beforeEach(async () => { + ctrl.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; + ctrl.target.aggregation.perSeriesAligner = 'ALIGN_NONE'; + ctrl.onAggregationChange('ALIGN_NONE'); + }); + + it('should set an alignment', () => { + expect(ctrl.target.aggregation.perSeriesAligner).not.toBe('ALIGN_NONE'); + }); + }); + }); +}); + +function createCtrlWithFakes() { + StackdriverAggregationCtrl.prototype.target = createTarget(); + return new StackdriverAggregationCtrl({ refresh: () => {} }); +} + +function createTarget(existingFilters?: string[]) { + return { + project: { + id: '', + name: '', + }, + metricType: 'ametric', + refId: 'A', + aggregation: { + crossSeriesReducer: '', + alignmentPeriod: '', + perSeriesAligner: '', + groupBys: [], + }, + filters: existingFilters || [], + aliasBy: '', + }; +} From e8cc0f3fff88f12a9759cbc30efc158a636901d7 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 25 Sep 2018 14:53:55 +0200 Subject: [PATCH 171/878] render list --- .../app/features/plugins/PluginActionBar.tsx | 24 +++++++++ public/app/features/plugins/PluginList.tsx | 21 ++++++++ .../app/features/plugins/PluginListItem.tsx | 30 +++++++++++ .../app/features/plugins/PluginListPage.tsx | 53 +++++++++++++++++++ public/app/features/plugins/state/actions.ts | 28 ++++++++++ public/app/features/plugins/state/reducers.ts | 16 ++++++ .../app/features/plugins/state/selectors.ts | 1 + public/app/routes/routes.ts | 8 +-- public/app/store/configureStore.ts | 2 + public/app/types/index.ts | 5 +- public/app/types/plugins.ts | 30 +++++++++++ 11 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 public/app/features/plugins/PluginActionBar.tsx create mode 100644 public/app/features/plugins/PluginList.tsx create mode 100644 public/app/features/plugins/PluginListItem.tsx create mode 100644 public/app/features/plugins/PluginListPage.tsx create mode 100644 public/app/features/plugins/state/actions.ts create mode 100644 public/app/features/plugins/state/reducers.ts create mode 100644 public/app/features/plugins/state/selectors.ts diff --git a/public/app/features/plugins/PluginActionBar.tsx b/public/app/features/plugins/PluginActionBar.tsx new file mode 100644 index 00000000000..e420bc3eca6 --- /dev/null +++ b/public/app/features/plugins/PluginActionBar.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export default function({ searchQuery, onQueryChange }) { + return ( +
    +
    + +
    + + ); +} diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx new file mode 100644 index 00000000000..02d7dac0dce --- /dev/null +++ b/public/app/features/plugins/PluginList.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import classNames from 'classnames/bind'; +import PluginListItem from './PluginListItem'; + +export default function PluginList({ plugins, layout }) { + const listStyle = classNames({ + 'card-section': true, + 'card-list-layout-grid': layout === 'grid', + 'card-list-layout-list': layout === 'list', + }); + + return ( +
    +
      + {plugins.map((plugin, index) => { + return ; + })} +
    +
    + ); +} diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx new file mode 100644 index 00000000000..a143625459a --- /dev/null +++ b/public/app/features/plugins/PluginListItem.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +export default function PluginListItem({ plugin }) { + return ( +
  • + +
    +
    + + {plugin.type} +
    + {plugin.hasUpdate && ( +
    + Update available! +
    + )} +
    +
    +
    + +
    +
    +
    {plugin.name}
    +
    {`By ${plugin.info.author.name}`}
    +
    +
    +
    +
  • + ); +} diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx new file mode 100644 index 00000000000..73837cfe75f --- /dev/null +++ b/public/app/features/plugins/PluginListPage.tsx @@ -0,0 +1,53 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from '../../core/components/PageHeader/PageHeader'; +import PluginActionBar from './PluginActionBar'; +import PluginList from './PluginList'; +import { NavModel, Plugin } from '../../types'; +import { loadPlugins } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getPlugins } from './state/selectors'; + +interface Props { + navModel: NavModel; + plugins: Plugin[]; + loadPlugins: typeof loadPlugins; +} + +export class PluginListPage extends PureComponent { + componentDidMount() { + this.fetchPlugins(); + } + + async fetchPlugins() { + await this.props.loadPlugins(); + } + + render() { + const { navModel, plugins } = this.props; + + return ( +
    + +
    + {}} /> + {plugins && } +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'plugins'), + plugins: getPlugins(state.plugins), + }; +} + +const mapDispatchToProps = { + loadPlugins, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage)); diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts new file mode 100644 index 00000000000..d044a7ba56f --- /dev/null +++ b/public/app/features/plugins/state/actions.ts @@ -0,0 +1,28 @@ +import { Plugin, StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from '../../../core/services/backend_srv'; + +export enum ActionTypes { + LoadPlugins = 'LOAD_PLUGINS', +} + +export interface LoadPluginsAction { + type: ActionTypes.LoadPlugins; + payload: Plugin[]; +} + +export const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ + type: ActionTypes.LoadPlugins, + payload: plugins, +}); + +export type Action = LoadPluginsAction; + +type ThunkResult = ThunkAction; + +export function loadPlugins(): ThunkResult { + return async dispatch => { + const result = await getBackendSrv().get('api/plugins', { embedded: 0 }); + dispatch(pluginsLoaded(result)); + }; +} diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts new file mode 100644 index 00000000000..af4089220b6 --- /dev/null +++ b/public/app/features/plugins/state/reducers.ts @@ -0,0 +1,16 @@ +import { Action, ActionTypes } from './actions'; +import { Plugin, PluginsState } from 'app/types'; + +export const initialState: PluginsState = { plugins: [] as Plugin[] }; + +export const pluginsReducer = (state = initialState, action: Action): PluginsState => { + switch (action.type) { + case ActionTypes.LoadPlugins: + return { ...state, plugins: action.payload }; + } + return state; +}; + +export default { + plugins: pluginsReducer, +}; diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts new file mode 100644 index 00000000000..d436e9fa016 --- /dev/null +++ b/public/app/features/plugins/state/selectors.ts @@ -0,0 +1 @@ +export const getPlugins = state => state.plugins; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 015b4ae0b51..e4662c77367 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,6 +5,7 @@ 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 PluginListPage from 'app/features/plugins/PluginListPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; @@ -245,9 +246,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/plugins', { - templateUrl: 'public/app/features/plugins/partials/plugin_list.html', - controller: 'PluginListCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => PluginListPage, + }, }) .when('/plugins/:pluginId/edit', { templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 8f6cf25043d..08d3d5bede0 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -6,6 +6,7 @@ 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'; +import pluginReducers from 'app/features/plugins/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, @@ -13,6 +14,7 @@ const rootReducer = combineReducers({ ...teamsReducers, ...foldersReducers, ...dashboardReducers, + ...pluginReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 778a1b21b55..4ec5c6f02cc 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -6,7 +6,7 @@ import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; -import { PluginMeta } from './plugins'; +import { PluginMeta, Plugin, PluginInfo, PluginsState } from './plugins'; export { Team, @@ -33,6 +33,9 @@ export { PermissionLevel, DataSource, PluginMeta, + PluginInfo, + Plugin, + PluginsState, }; export interface StoreState { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index d26085f8e73..e1594296acb 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -17,3 +17,33 @@ export interface PluginMetaInfo { small: string; }; } + +export interface PluginInfo { + author: { + name: string; + url: string; + }; + description: string; + links: string[]; + logos: { small: string; large: string }; + screenshots: string; + updated: string; + version: string; +} + +export interface Plugin { + defaultNavUrl: string; + enabled: boolean; + hasUpdate: boolean; + id: string; + info: PluginInfo; + latestVersion: string; + name: string; + pinned: boolean; + state: string; + type: string; +} + +export interface PluginsState { + plugins: Plugin[]; +} From 7f43909390ea3115a6911adcd57c5f71598c1076 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 15:16:33 +0200 Subject: [PATCH 172/878] stackdriver: typescriptifying controller --- .../datasource/stackdriver/query_aggregation_ctrl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 79161e0ca8c..d30fb7626cc 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -22,7 +22,7 @@ export class StackdriverAggregationCtrl { aggOptions: any[]; refresh: () => void; - constructor(private $scope) { + constructor($scope) { this.aggOptions = options.aggOptions; this.alignOptions = options.alignOptions; $scope.alignmentPeriods = options.alignmentPeriods; @@ -33,14 +33,14 @@ export class StackdriverAggregationCtrl { this.refresh = $scope.refresh; } - onAlignmentChange(newVal) { + onAlignmentChange(newVal: string) { if (newVal === 'ALIGN_NONE') { this.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; } this.refresh(); } - onAggregationChange(newVal) { + onAggregationChange(newVal: string) { if (newVal !== 'REDUCE_NONE') { const newAlignmentOption = options.alignOptions.find(o => o.value !== 'ALIGN_NONE'); this.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; From 0b7576a1f92992e87fb90ef6d519c676a81d5236 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 25 Sep 2018 16:21:52 +0200 Subject: [PATCH 173/878] filter plugins and layout mode --- .../LayoutSelector/LayoutSelector.tsx | 24 ++++++ .../app/features/plugins/PluginActionBar.tsx | 82 ++++++++++++++----- .../app/features/plugins/PluginListPage.tsx | 8 +- public/app/features/plugins/state/actions.ts | 26 +++++- public/app/features/plugins/state/reducers.ts | 8 +- .../app/features/plugins/state/selectors.ts | 11 ++- public/app/types/plugins.ts | 2 + 7 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 public/app/core/components/LayoutSelector/LayoutSelector.tsx diff --git a/public/app/core/components/LayoutSelector/LayoutSelector.tsx b/public/app/core/components/LayoutSelector/LayoutSelector.tsx new file mode 100644 index 00000000000..85322dc9da0 --- /dev/null +++ b/public/app/core/components/LayoutSelector/LayoutSelector.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export default function LayoutSelector({ mode, onLayoutModeChanged }) { + return ( +
    + + +
    + ); +} diff --git a/public/app/features/plugins/PluginActionBar.tsx b/public/app/features/plugins/PluginActionBar.tsx index e420bc3eca6..12b1353e9dd 100644 --- a/public/app/features/plugins/PluginActionBar.tsx +++ b/public/app/features/plugins/PluginActionBar.tsx @@ -1,24 +1,62 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import LayoutSelector from '../../core/components/LayoutSelector/LayoutSelector'; +import { setLayoutMode, setPluginsSearchQuery } from './state/actions'; +import { getPluginsSearchQuery, getLayoutMode } from './state/selectors'; -export default function({ searchQuery, onQueryChange }) { - return ( -
    -
    - -
    - - ); +export interface Props { + searchQuery: string; + layoutMode: string; + setLayoutMode: typeof setLayoutMode; + setPluginsSearchQuery: typeof setPluginsSearchQuery; } + +export class PluginActionBar extends PureComponent { + onSearchQueryChange = event => { + this.props.setPluginsSearchQuery(event.target.value); + }; + + render() { + const { searchQuery, layoutMode, setLayoutMode } = this.props; + + return ( +
    +
    + + setLayoutMode(mode)} /> +
    + + ); + } +} + +function mapStateToProps(state) { + return { + searchQuery: getPluginsSearchQuery(state.plugins), + layoutMode: getLayoutMode(state.plugins), + }; +} + +const mapDispatchToProps = { + setPluginsSearchQuery, + setLayoutMode, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 73837cfe75f..a2b0348aefd 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -7,11 +7,12 @@ import PluginList from './PluginList'; import { NavModel, Plugin } from '../../types'; import { loadPlugins } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; -import { getPlugins } from './state/selectors'; +import { getLayoutMode, getPlugins } from './state/selectors'; interface Props { navModel: NavModel; plugins: Plugin[]; + layoutMode: string; loadPlugins: typeof loadPlugins; } @@ -25,14 +26,14 @@ export class PluginListPage extends PureComponent { } render() { - const { navModel, plugins } = this.props; + const { navModel, plugins, layoutMode } = this.props; return (
    {}} /> - {plugins && } + {plugins && }
    ); @@ -43,6 +44,7 @@ function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'plugins'), plugins: getPlugins(state.plugins), + layoutMode: getLayoutMode(state.plugins), }; } diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts index d044a7ba56f..b842037ffc7 100644 --- a/public/app/features/plugins/state/actions.ts +++ b/public/app/features/plugins/state/actions.ts @@ -4,6 +4,8 @@ import { getBackendSrv } from '../../../core/services/backend_srv'; export enum ActionTypes { LoadPlugins = 'LOAD_PLUGINS', + SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY', + SetLayoutMode = 'SET_LAYOUT_MODE', } export interface LoadPluginsAction { @@ -11,12 +13,32 @@ export interface LoadPluginsAction { payload: Plugin[]; } -export const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ +export interface SetPluginsSearchQueryAction { + type: ActionTypes.SetPluginsSearchQuery; + payload: string; +} + +export interface SetLayoutModeAction { + type: ActionTypes.SetLayoutMode; + payload: string; +} + +export const setLayoutMode = (mode: string): SetLayoutModeAction => ({ + type: ActionTypes.SetLayoutMode, + payload: mode, +}); + +export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryAction => ({ + type: ActionTypes.SetPluginsSearchQuery, + payload: query, +}); + +const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ type: ActionTypes.LoadPlugins, payload: plugins, }); -export type Action = LoadPluginsAction; +export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction; type ThunkResult = ThunkAction; diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts index af4089220b6..aadf78afe5e 100644 --- a/public/app/features/plugins/state/reducers.ts +++ b/public/app/features/plugins/state/reducers.ts @@ -1,12 +1,18 @@ import { Action, ActionTypes } from './actions'; import { Plugin, PluginsState } from 'app/types'; -export const initialState: PluginsState = { plugins: [] as Plugin[] }; +export const initialState: PluginsState = { plugins: [] as Plugin[], searchQuery: '', layoutMode: 'grid' }; export const pluginsReducer = (state = initialState, action: Action): PluginsState => { switch (action.type) { case ActionTypes.LoadPlugins: return { ...state, plugins: action.payload }; + + case ActionTypes.SetPluginsSearchQuery: + return { ...state, searchQuery: action.payload }; + + case ActionTypes.SetLayoutMode: + return { ...state, layoutMode: action.payload }; } return state; }; diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts index d436e9fa016..80c74649a4b 100644 --- a/public/app/features/plugins/state/selectors.ts +++ b/public/app/features/plugins/state/selectors.ts @@ -1 +1,10 @@ -export const getPlugins = state => state.plugins; +export const getPlugins = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.plugins.filter(item => { + return regex.test(item.name); + }); +}; + +export const getPluginsSearchQuery = state => state.searchQuery; +export const getLayoutMode = state => state.layoutMode; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index e1594296acb..25c5b13153e 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -46,4 +46,6 @@ export interface Plugin { export interface PluginsState { plugins: Plugin[]; + searchQuery: string; + layoutMode: string; } From a41c5f7b378c782f8ff52c9d465893e7b2c46141 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 16:40:15 +0200 Subject: [PATCH 174/878] stackdriver: remove console.log --- .../datasource/stackdriver/specs/query_aggregation_ctrl.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index aebd2a12dd6..bf32f1978cd 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -23,7 +23,6 @@ describe('StackdriverAggregationCtrl', () => { it('should populate all alignment options except two', () => { const result = ctrl.getAlignOptions(); - console.log(result.map(o => o.value)); expect(result.length).toBe(10); expect(result.map(o => o.value)).toEqual( expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) From 64eace96c0a3b62895525be1ce4cd59897bfb7c0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 25 Sep 2018 16:50:13 +0200 Subject: [PATCH 175/878] first test --- .../features/plugins/PluginListPage.test.tsx | 31 +++++++++++++++++++ .../app/features/plugins/PluginListPage.tsx | 2 +- .../PluginListPage.test.tsx.snap | 21 +++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 public/app/features/plugins/PluginListPage.test.tsx create mode 100644 public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx new file mode 100644 index 00000000000..830d1176eae --- /dev/null +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { PluginListPage, Props } from './PluginListPage'; +import { NavModel, Plugin } from '../../types'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + plugins: [] as Plugin[], + layoutMode: 'grid', + loadPlugins: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as PluginListPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index a2b0348aefd..358eb4a7887 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -9,7 +9,7 @@ import { loadPlugins } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; import { getLayoutMode, getPlugins } from './state/selectors'; -interface Props { +export interface Props { navModel: NavModel; plugins: Plugin[]; layoutMode: string; diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap new file mode 100644 index 00000000000..cb8c79bbcee --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    + + +
    +
    +`; From 3f7314831fbba07f860296f84f80595145f88b8e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 16:51:12 +0200 Subject: [PATCH 176/878] stackdriver: wip: split metric dropdown into two parts - resource and metric --- .../datasource/stackdriver/datasource.ts | 2 +- .../stackdriver/partials/query.editor.html | 22 +++++++- .../datasource/stackdriver/query_ctrl.ts | 56 +++++++++++++++++-- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7d7b1e1cee1..5b6b5fd2a04 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -159,7 +159,7 @@ export default class StackdriverDatasource { try { const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`; const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); - return data.metricDescriptors.map(m => ({ id: m.type, name: m.displayName })); + return data.metricDescriptors; } catch (error) { console.log(error); } diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 71ae3dc6328..6696a57fa4d 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -1,5 +1,5 @@ -
    + +
    +
    + Resource type + +
    +
    +
    +
    +
    +
    +
    + Metric + +
    +
    +
    +
    diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 3f9cda2a875..e4a84916d46 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -19,6 +19,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: string; }; metricType: string; + metricService: string; refId: string; aggregation: { crossSeriesReducer: string; @@ -32,6 +33,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: any; }; defaultDropdownValue = 'select metric'; + defaultMetricResourcesValue = 'all'; defaultRemoveGroupByValue = '-- remove group by --'; loadLabelsPromise: Promise; stackdriverConstants; @@ -42,6 +44,8 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: 'loading project...', }, metricType: this.defaultDropdownValue, + metricService: this.defaultMetricResourcesValue, + metric: '', aggregation: { crossSeriesReducer: 'REDUCE_MEAN', alignmentPeriod: 'auto', @@ -55,6 +59,8 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: '', }; + metricDescriptors: any[]; + metrics: any[]; groupBySegments: any[]; removeSegment: any; showHelp: boolean; @@ -69,10 +75,10 @@ export class StackdriverQueryCtrl extends QueryCtrl { constructor($scope, $injector, private uiSegmentSrv, private templateSrv) { super($scope, $injector); _.defaultsDeep(this.target, this.defaults); - + this.metricDescriptors = []; + this.metrics = []; this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); - this.getCurrentProject() .then(this.getMetricTypes.bind(this)) .then(this.getLabels.bind(this)); @@ -118,18 +124,60 @@ export class StackdriverQueryCtrl extends QueryCtrl { } async getMetricTypes() { - //projects/your-project-name/metricDescriptors/agent.googleapis.com/agent/api_request_count if (this.target.project.id !== 'default') { const metricTypes = await this.datasource.getMetricTypes(this.target.project.id); + this.metricDescriptors = metricTypes; if (this.target.metricType === this.defaultDropdownValue && metricTypes.length > 0) { this.$scope.$apply(() => (this.target.metricType = metricTypes[0].id)); } - return metricTypes.map(mt => ({ value: mt.id, text: mt.id })); + + return metricTypes.map(mt => ({ value: mt.type, text: mt.type })); } else { return []; } } + getMetricServices() { + const defaultValue = { value: this.defaultMetricResourcesValue, text: this.defaultMetricResourcesValue }; + const resources = this.metricDescriptors.map(m => { + const [resource] = m.type.split('/'); + const [service] = resource.split('.'); + return { + value: resource, + text: service, + }; + }); + return resources.length > 0 ? [defaultValue, ..._.uniqBy(resources, 'value')] : []; + } + + getMetrics() { + const metrics = this.metricDescriptors.map(m => { + const [resource] = m.type.split('/'); + const [service] = resource.split('.'); + return { + resource, + value: m.type, + service, + text: m.displayName, + title: m.description, + }; + }); + if (this.target.metricService === this.defaultMetricResourcesValue) { + return metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); + } else { + return metrics.filter(m => m.resource === this.target.metricService); + } + } + + onResourceTypeChange(resource) { + this.metrics = this.getMetrics(); + if (!this.metrics.find(m => m.value === this.target.metricType)) { + this.target.metricType = this.defaultDropdownValue; + } else { + this.refresh(); + } + } + async getLabels() { this.loadLabelsPromise = new Promise(async resolve => { try { From 38e32a902e6632be764d9f624ca472656315d254 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 16:51:39 +0200 Subject: [PATCH 177/878] stackdriver: fix failing test --- .../plugins/datasource/stackdriver/specs/datasource.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 541dbfaa7e8..58575b577b9 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -180,8 +180,8 @@ describe('StackdriverDataSource', () => { }); it('should return successfully', () => { expect(result.length).toBe(2); - expect(result[0].id).toBe('test metric type 1'); - expect(result[0].name).toBe('test metric name 1'); + expect(result[0].type).toBe('test metric type 1'); + expect(result[0].displayName).toBe('test metric name 1'); }); }); From 2d602bfcf3f9575f00e38c9cd02df33a6a88f947 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 Sep 2018 17:03:10 +0200 Subject: [PATCH 178/878] stackdriver: improve aggregation logic --- .../datasource/stackdriver/query_aggregation_ctrl.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index d30fb7626cc..02dc3ec6d67 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -41,8 +41,13 @@ export class StackdriverAggregationCtrl { } onAggregationChange(newVal: string) { - if (newVal !== 'REDUCE_NONE') { - const newAlignmentOption = options.alignOptions.find(o => o.value !== 'ALIGN_NONE'); + if (newVal !== 'REDUCE_NONE' && this.target.aggregation.perSeriesAligner === 'ALIGN_NONE') { + const newAlignmentOption = options.alignOptions.find( + o => + o.value !== 'ALIGN_NONE' && + o.valueTypes.indexOf(this.target.valueType) !== -1 && + o.metricKinds.indexOf(this.target.metricKind) !== -1 + ); this.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; } this.refresh(); From 90595ffdcee365a3b088e68bcd84885ed31e182a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 25 Sep 2018 21:31:24 +0200 Subject: [PATCH 179/878] cli: fix init of bus --- pkg/cmd/grafana-cli/commands/commands.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 5e69559b9fa..902fd415977 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -6,6 +6,7 @@ import ( "github.com/codegangsta/cli" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" @@ -24,6 +25,7 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli engine := &sqlstore.SqlStore{} engine.Cfg = cfg + engine.Bus = bus.GetBus() engine.Init() if err := command(cmd); err != nil { From 0a77cd55698adbcf610abb0abaf9b71a8d1485e9 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 26 Sep 2018 00:33:18 +0200 Subject: [PATCH 180/878] stackdriver: adds on-change with debounce for alias by field --- package.json | 2 +- .../datasource/stackdriver/partials/query.editor.html | 4 ++-- .../plugins/datasource/stackdriver/specs/query_ctrl.test.ts | 3 +++ yarn.lock | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1e7ed02c87b..ba48304f0cd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@types/d3": "^4.10.1", "@types/enzyme": "^3.1.13", - "@types/jest": "^21.1.4", + "@types/jest": "^23.3.2", "@types/node": "^8.0.31", "@types/react": "^16.4.14", "@types/react-custom-scrollbars": "^4.0.5", diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 6696a57fa4d..bcb499702ce 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -55,7 +55,7 @@
    Alias By - +
    @@ -109,4 +109,4 @@
    {{ctrl.lastQueryError}}
    - \ No newline at end of file + diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts index 590dea22601..0428cb3c618 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts @@ -427,5 +427,8 @@ function createTarget(existingFilters?: string[]) { }, filters: existingFilters || [], aliasBy: '', + metricService: '', + metricKind: '', + valueType: '', }; } diff --git a/yarn.lock b/yarn.lock index 413ff19e472..c98bec855e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -228,9 +228,9 @@ 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/jest@^23.3.2": + version "23.3.2" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.2.tgz#07b90f6adf75d42c34230c026a2529e56c249dbb" "@types/node@*": version "10.9.4" From e9790c9f1b340cbf9c3918d0dbcc51af11abdf6c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 26 Sep 2018 07:56:33 +0200 Subject: [PATCH 181/878] changed to first and last child --- public/sass/components/_buttons.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 99982388e5c..916c141a5ec 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -228,11 +228,11 @@ $btn-service-icon-width: 35px; color: $text-color-weak; box-shadow: $card-shadow; - &:nth-child(1) { + &:first-child { border-radius: 2px 0 0 2px; margin: 0; } - &:nth-child(2) { + &:last-child { border-radius: 0 2px 2px 0; margin-left: 0 !important; } From 49cd31ab7863ab06721d2eb723be011d7def9efa Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 11:17:24 +0200 Subject: [PATCH 182/878] stackdriver: get value type and metric kind from metric descriptor instead of from latest metric result --- pkg/tsdb/stackdriver/stackdriver.go | 3 -- .../stackdriver/partials/query.editor.html | 6 ++-- .../stackdriver/query_aggregation_ctrl.ts | 4 +-- .../datasource/stackdriver/query_ctrl.ts | 32 ++++++++----------- .../stackdriver/specs/query_ctrl.test.ts | 4 +-- 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index ffd065d6a18..3a903a544be 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -316,9 +316,6 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta Name: metricName, Points: points, }) - - queryRes.Meta.Set("metricKind", series.MetricKind) - queryRes.Meta.Set("valueType", series.ValueType) } queryRes.Meta.Set("resourceLabels", resourceLabels) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 6696a57fa4d..979557cf1e3 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -12,7 +12,7 @@
    Resource type -
    @@ -22,8 +22,8 @@
    Metric - +
    diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 02dc3ec6d67..f1c86aede9a 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -55,7 +55,7 @@ export class StackdriverAggregationCtrl { getAlignOptions() { return !this.target.valueType - ? options.alignOptions + ? [] : options.alignOptions.filter(i => { return ( i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 @@ -65,7 +65,7 @@ export class StackdriverAggregationCtrl { getAggOptions() { return !this.target.metricKind - ? options.aggOptions + ? [] : options.aggOptions.filter(i => { return ( i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index e4a84916d46..0babdf8fb7b 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -19,7 +19,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: string; }; metricType: string; - metricService: string; + resourceType: string; refId: string; aggregation: { crossSeriesReducer: string; @@ -44,7 +44,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: 'loading project...', }, metricType: this.defaultDropdownValue, - metricService: this.defaultMetricResourcesValue, + resourceType: this.defaultMetricResourcesValue, metric: '', aggregation: { crossSeriesReducer: 'REDUCE_MEAN', @@ -80,7 +80,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); this.getCurrentProject() - .then(this.getMetricTypes.bind(this)) + .then(this.loadMetricDescriptors.bind(this)) .then(this.getLabels.bind(this)); this.initSegments(); } @@ -123,21 +123,17 @@ export class StackdriverQueryCtrl extends QueryCtrl { } } - async getMetricTypes() { + async loadMetricDescriptors() { if (this.target.project.id !== 'default') { - const metricTypes = await this.datasource.getMetricTypes(this.target.project.id); - this.metricDescriptors = metricTypes; - if (this.target.metricType === this.defaultDropdownValue && metricTypes.length > 0) { - this.$scope.$apply(() => (this.target.metricType = metricTypes[0].id)); - } - - return metricTypes.map(mt => ({ value: mt.type, text: mt.type })); + this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); + this.metrics = this.getMetrics(); + return this.metricDescriptors; } else { return []; } } - getMetricServices() { + getResourceTypes() { const defaultValue = { value: this.defaultMetricResourcesValue, text: this.defaultMetricResourcesValue }; const resources = this.metricDescriptors.map(m => { const [resource] = m.type.split('/'); @@ -162,10 +158,11 @@ export class StackdriverQueryCtrl extends QueryCtrl { title: m.description, }; }); - if (this.target.metricService === this.defaultMetricResourcesValue) { + + if (this.target.resourceType === this.defaultMetricResourcesValue) { return metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); } else { - return metrics.filter(m => m.resource === this.target.metricService); + return metrics.filter(m => m.resource === this.target.resourceType); } } @@ -182,12 +179,8 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.loadLabelsPromise = new Promise(async resolve => { try { const data = await this.datasource.getLabels(this.target.metricType, this.target.refId); - this.metricLabels = data.results[this.target.refId].meta.metricLabels; this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; - - this.target.valueType = data.results[this.target.refId].meta.valueType; - this.target.metricKind = data.results[this.target.refId].meta.metricKind; resolve(); } catch (error) { console.log(error.data.message); @@ -198,6 +191,9 @@ export class StackdriverQueryCtrl extends QueryCtrl { } async onMetricTypeChange() { + const { valueType, metricKind } = this.metricDescriptors.find(m => m.type === this.target.metricType); + this.target.valueType = valueType; + this.target.metricKind = metricKind; this.refresh(); this.getLabels(); } diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts index 590dea22601..cbe91fbc6eb 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts @@ -373,8 +373,8 @@ function createCtrlWithFakes(existingFilters?: string[]) { refresh: () => {}, }; StackdriverQueryCtrl.prototype.target = createTarget(existingFilters); - StackdriverQueryCtrl.prototype.getMetricTypes = () => { - return Promise.resolve(); + StackdriverQueryCtrl.prototype.loadMetricDescriptors = () => { + return Promise.resolve([]); }; StackdriverQueryCtrl.prototype.getLabels = () => { return Promise.resolve(); From 508601c28cdaf9c62a063d20b3aa0d7eb7e871c8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 11:23:46 +0200 Subject: [PATCH 183/878] stackdriver: use correct naming convention --- .../stackdriver/partials/query.editor.html | 11 +++--- .../datasource/stackdriver/query_ctrl.ts | 36 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 16b8e8db6d6..ff3d974b433 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -11,9 +11,9 @@
    -->
    - Resource type - + Service +
    @@ -55,7 +55,8 @@
    Alias By - +
    @@ -109,4 +110,4 @@
    {{ctrl.lastQueryError}}
    - + \ 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 index 0babdf8fb7b..c6629748a3a 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -19,7 +19,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: string; }; metricType: string; - resourceType: string; + service: string; refId: string; aggregation: { crossSeriesReducer: string; @@ -33,7 +33,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: any; }; defaultDropdownValue = 'select metric'; - defaultMetricResourcesValue = 'all'; + defaultServiceValue = 'all'; defaultRemoveGroupByValue = '-- remove group by --'; loadLabelsPromise: Promise; stackdriverConstants; @@ -44,7 +44,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: 'loading project...', }, metricType: this.defaultDropdownValue, - resourceType: this.defaultMetricResourcesValue, + service: this.defaultServiceValue, metric: '', aggregation: { crossSeriesReducer: 'REDUCE_MEAN', @@ -133,40 +133,40 @@ export class StackdriverQueryCtrl extends QueryCtrl { } } - getResourceTypes() { - const defaultValue = { value: this.defaultMetricResourcesValue, text: this.defaultMetricResourcesValue }; - const resources = this.metricDescriptors.map(m => { - const [resource] = m.type.split('/'); - const [service] = resource.split('.'); + getServices() { + const defaultValue = { value: this.defaultServiceValue, text: this.defaultServiceValue }; + const services = this.metricDescriptors.map(m => { + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); return { - value: resource, - text: service, + value: service, + text: serviceShortName, }; }); - return resources.length > 0 ? [defaultValue, ..._.uniqBy(resources, 'value')] : []; + return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : []; } getMetrics() { const metrics = this.metricDescriptors.map(m => { - const [resource] = m.type.split('/'); - const [service] = resource.split('.'); + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); return { - resource, - value: m.type, service, + value: m.type, + serviceShortName, text: m.displayName, title: m.description, }; }); - if (this.target.resourceType === this.defaultMetricResourcesValue) { + if (this.target.service === this.defaultServiceValue) { return metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); } else { - return metrics.filter(m => m.resource === this.target.resourceType); + return metrics.filter(m => m.service === this.target.service); } } - onResourceTypeChange(resource) { + onServiceChange() { this.metrics = this.getMetrics(); if (!this.metrics.find(m => m.value === this.target.metricType)) { this.target.metricType = this.defaultDropdownValue; From 1a91e0baf62f08651ace34eb39bdee8e6835c945 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 13:22:20 +0200 Subject: [PATCH 184/878] stackdriver: update aggregation and alignment before refreshing when changing metric --- .../partials/query.aggregation.html | 4 +- .../stackdriver/query_aggregation_ctrl.ts | 54 +++++++++++-------- .../datasource/stackdriver/query_ctrl.ts | 1 + 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html index f48d0d9f565..d0eb33f649c 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html @@ -2,7 +2,7 @@
    -
    @@ -20,7 +20,7 @@
    -
    diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index f1c86aede9a..ad0b048713b 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -20,57 +20,65 @@ export class StackdriverAggregationCtrl { target: any; alignOptions: any[]; aggOptions: any[]; - refresh: () => void; - constructor($scope) { - this.aggOptions = options.aggOptions; - this.alignOptions = options.alignOptions; - $scope.alignmentPeriods = options.alignmentPeriods; - $scope.getAlignOptions = this.getAlignOptions; - $scope.getAggOptions = this.getAggOptions; - $scope.onAlignmentChange = this.onAlignmentChange; - $scope.onAggregationChange = this.onAggregationChange; - this.refresh = $scope.refresh; + constructor(private $scope) { + $scope.aggOptions = options.aggOptions; + this.setAggOptions(); + this.setAlignOptions(); + $scope.onAlignmentChange = this.onAlignmentChange.bind(this); + $scope.onAggregationChange = this.onAggregationChange.bind(this); + $scope.$on('metricTypeChange', this.setAlignOptions.bind(this)); } onAlignmentChange(newVal: string) { if (newVal === 'ALIGN_NONE') { - this.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; + this.$scope.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; } - this.refresh(); + this.$scope.refresh(); } onAggregationChange(newVal: string) { - if (newVal !== 'REDUCE_NONE' && this.target.aggregation.perSeriesAligner === 'ALIGN_NONE') { + if (newVal !== 'REDUCE_NONE' && this.$scope.target.aggregation.perSeriesAligner === 'ALIGN_NONE') { const newAlignmentOption = options.alignOptions.find( o => o.value !== 'ALIGN_NONE' && - o.valueTypes.indexOf(this.target.valueType) !== -1 && - o.metricKinds.indexOf(this.target.metricKind) !== -1 + o.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && + o.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 ); - this.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; + this.$scope.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; } - this.refresh(); + this.$scope.refresh(); } - getAlignOptions() { - return !this.target.valueType + setAlignOptions() { + this.$scope.alignOptions = !this.$scope.target.valueType ? [] : options.alignOptions.filter(i => { return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 + i.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && + i.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 ); }); + if (!this.$scope.alignOptions.find(o => o.value === this.$scope.target.aggregation.perSeriesAligner)) { + const newValue = this.$scope.alignOptions.find(o => o.value !== 'ALIGN_NONE'); + this.$scope.target.aggregation.perSeriesAligner = newValue ? newValue.value : ''; + } } - getAggOptions() { - return !this.target.metricKind + setAggOptions() { + this.$scope.aggOptions = !this.$scope.target.metricKind ? [] : options.aggOptions.filter(i => { return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 + i.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && + i.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 ); }); + + if (!this.$scope.aggOptions.find(o => o.value === this.$scope.target.aggregation.crossSeriesReducer)) { + const newValue = this.$scope.aggOptions.find(o => o.value !== 'REDUCE_NONE'); + this.$scope.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + } } } diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index c6629748a3a..7f9b473a15a 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -194,6 +194,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { const { valueType, metricKind } = this.metricDescriptors.find(m => m.type === this.target.metricType); this.target.valueType = valueType; this.target.metricKind = metricKind; + this.$scope.$broadcast('metricTypeChange'); this.refresh(); this.getLabels(); } From 85fce840879b2256ef619e8062596a9f9deaca05 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 13:39:03 +0200 Subject: [PATCH 185/878] stackdriver: fix broken tests --- .../stackdriver/query_aggregation_ctrl.ts | 4 -- .../specs/query_aggregation_ctrl.test.ts | 64 +++++++------------ 2 files changed, 23 insertions(+), 45 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index ad0b048713b..72f3ca47a13 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -17,10 +17,6 @@ export class StackdriverAggregation { } export class StackdriverAggregationCtrl { - target: any; - alignOptions: any[]; - aggOptions: any[]; - constructor(private $scope) { $scope.aggOptions = options.aggOptions; this.setAggOptions(); diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index bf32f1978cd..af8d8aee3c7 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -3,78 +3,60 @@ import { StackdriverAggregationCtrl } from '../query_aggregation_ctrl'; describe('StackdriverAggregationCtrl', () => { let ctrl; describe('aggregation and alignment options', () => { - beforeEach(() => { - ctrl = createCtrlWithFakes(); - }); describe('when new query result is returned from the server', () => { describe('and result is double and gauge', () => { beforeEach(async () => { - ctrl.target.valueType = 'DOUBLE'; - ctrl.target.metricKind = 'GAUGE'; + ctrl = new StackdriverAggregationCtrl({ + $on: () => {}, + target: { valueType: 'DOUBLE', metricKind: 'GAUGE', aggregation: { crossSeriesReducer: '' } }, + }); }); it('should populate all aggregate options except two', () => { - const result = ctrl.getAggOptions(); - expect(result.length).toBe(11); - expect(result.map(o => o.value)).toEqual( + ctrl.setAggOptions(); + expect(ctrl.$scope.aggOptions.length).toBe(11); + expect(ctrl.$scope.aggOptions.map(o => o.value)).toEqual( expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); it('should populate all alignment options except two', () => { - const result = ctrl.getAlignOptions(); - expect(result.length).toBe(10); - expect(result.map(o => o.value)).toEqual( + ctrl.setAlignOptions(); + expect(ctrl.$scope.alignOptions.length).toBe(10); + expect(ctrl.$scope.alignOptions.map(o => o.value)).toEqual( expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); }); }); - describe('when a user a user select ALIGN_NONE and a reducer is selected', () => { + describe('when a user selects ALIGN_NONE and a reducer is selected', () => { beforeEach(async () => { - ctrl.target.aggregation.crossSeriesReducer = 'RANDOM_REDUCER'; + ctrl = new StackdriverAggregationCtrl({ + $on: () => {}, + refresh: () => {}, + target: { aggregation: { crossSeriesReducer: 'RANDOM_REDUCER' } }, + }); ctrl.onAlignmentChange('ALIGN_NONE'); }); it('should set REDUCE_NONE as selected aggregation', () => { - expect(ctrl.target.aggregation.crossSeriesReducer).toBe('REDUCE_NONE'); + expect(ctrl.$scope.target.aggregation.crossSeriesReducer).toBe('REDUCE_NONE'); }); }); describe('when a user a user select a reducer and no alignment is selected', () => { beforeEach(async () => { - ctrl.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; - ctrl.target.aggregation.perSeriesAligner = 'ALIGN_NONE'; + ctrl = new StackdriverAggregationCtrl({ + $on: () => {}, + refresh: () => {}, + target: { aggregation: { crossSeriesReducer: 'REDUCE_NONE', perSeriesAligner: 'ALIGN_NONE' } }, + }); ctrl.onAggregationChange('ALIGN_NONE'); }); it('should set an alignment', () => { - expect(ctrl.target.aggregation.perSeriesAligner).not.toBe('ALIGN_NONE'); + expect(ctrl.$scope.target.aggregation.perSeriesAligner).not.toBe('ALIGN_NONE'); }); }); }); }); - -function createCtrlWithFakes() { - StackdriverAggregationCtrl.prototype.target = createTarget(); - return new StackdriverAggregationCtrl({ refresh: () => {} }); -} - -function createTarget(existingFilters?: string[]) { - return { - project: { - id: '', - name: '', - }, - metricType: 'ametric', - refId: 'A', - aggregation: { - crossSeriesReducer: '', - alignmentPeriod: '', - perSeriesAligner: '', - groupBys: [], - }, - filters: existingFilters || [], - aliasBy: '', - }; -} From 2965f58838c676d2b693a7d501100ae6e01657d9 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 13:42:53 +0200 Subject: [PATCH 186/878] stackdriver: use correct event name --- .../plugins/datasource/stackdriver/query_aggregation_ctrl.ts | 2 +- public/app/plugins/datasource/stackdriver/query_ctrl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 72f3ca47a13..a4f523177a7 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -23,7 +23,7 @@ export class StackdriverAggregationCtrl { this.setAlignOptions(); $scope.onAlignmentChange = this.onAlignmentChange.bind(this); $scope.onAggregationChange = this.onAggregationChange.bind(this); - $scope.$on('metricTypeChange', this.setAlignOptions.bind(this)); + $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); } onAlignmentChange(newVal: string) { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 7f9b473a15a..4f21e60e123 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -194,7 +194,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { const { valueType, metricKind } = this.metricDescriptors.find(m => m.type === this.target.metricType); this.target.valueType = valueType; this.target.metricKind = metricKind; - this.$scope.$broadcast('metricTypeChange'); + this.$scope.$broadcast('metricTypeChanged'); this.refresh(); this.getLabels(); } From b883d7c1f3e2ba2b147641ad66fa6aeb25a22a51 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 15:01:02 +0200 Subject: [PATCH 187/878] stackdriver: make sure service and metric display name is used instead of value when loading a saved query editor --- .../stackdriver/partials/query.editor.html | 16 +---- .../datasource/stackdriver/query_ctrl.ts | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index ff3d974b433..6f3fb216d24 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -1,19 +1,9 @@ -
    Service - +
    @@ -22,7 +12,7 @@
    Metric -
    diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 4f21e60e123..b9eefcf3789 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -32,8 +32,8 @@ export class StackdriverQueryCtrl extends QueryCtrl { metricKind: any; valueType: any; }; - defaultDropdownValue = 'select metric'; - defaultServiceValue = 'all'; + defaultDropdownValue = 'Select Metric'; + defaultServiceValue = 'All Services'; defaultRemoveGroupByValue = '-- remove group by --'; loadLabelsPromise: Promise; stackdriverConstants; @@ -59,8 +59,11 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: '', }; + service: string; + metricType: string; metricDescriptors: any[]; metrics: any[]; + services: any[]; groupBySegments: any[]; removeSegment: any; showHelp: boolean; @@ -77,6 +80,9 @@ export class StackdriverQueryCtrl extends QueryCtrl { _.defaultsDeep(this.target, this.defaults); this.metricDescriptors = []; this.metrics = []; + this.services = []; + this.metricType = this.defaultDropdownValue; + this.service = this.defaultServiceValue; this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); this.getCurrentProject() @@ -126,14 +132,15 @@ export class StackdriverQueryCtrl extends QueryCtrl { async loadMetricDescriptors() { if (this.target.project.id !== 'default') { this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); - this.metrics = this.getMetrics(); + this.services = this.getServicesList(); + this.metrics = this.getMetricsList(); return this.metricDescriptors; } else { return []; } } - getServices() { + getServicesList() { const defaultValue = { value: this.defaultServiceValue, text: this.defaultServiceValue }; const services = this.metricDescriptors.map(m => { const [service] = m.type.split('/'); @@ -143,10 +150,15 @@ export class StackdriverQueryCtrl extends QueryCtrl { text: serviceShortName, }; }); + + if (services.find(m => m.value === this.target.service)) { + this.service = this.target.service; + } + return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : []; } - getMetrics() { + getMetricsList() { const metrics = this.metricDescriptors.map(m => { const [service] = m.type.split('/'); const [serviceShortName] = service.split('.'); @@ -159,20 +171,19 @@ export class StackdriverQueryCtrl extends QueryCtrl { }; }); + let result; if (this.target.service === this.defaultServiceValue) { - return metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); + result = metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); } else { - return metrics.filter(m => m.service === this.target.service); + result = metrics.filter(m => m.service === this.target.service); } - } - onServiceChange() { - this.metrics = this.getMetrics(); - if (!this.metrics.find(m => m.value === this.target.metricType)) { - this.target.metricType = this.defaultDropdownValue; - } else { - this.refresh(); + if (result.find(m => m.value === this.target.metricType)) { + this.metricType = this.target.metricType; + } else if (result.length > 0) { + this.metricType = this.target.metricType = result[0].value; } + return result; } async getLabels() { @@ -190,13 +201,29 @@ export class StackdriverQueryCtrl extends QueryCtrl { }); } + onServiceChange() { + this.target.service = this.service; + this.metrics = this.getMetricsList(); + this.setMetricType(); + if (!this.metrics.find(m => m.value === this.target.metricType)) { + this.target.metricType = this.defaultDropdownValue; + } else { + this.refresh(); + } + } + async onMetricTypeChange() { + this.setMetricType(); + this.refresh(); + this.getLabels(); + } + + setMetricType() { + this.target.metricType = this.metricType; const { valueType, metricKind } = this.metricDescriptors.find(m => m.type === this.target.metricType); this.target.valueType = valueType; this.target.metricKind = metricKind; this.$scope.$broadcast('metricTypeChanged'); - this.refresh(); - this.getLabels(); } async getGroupBys(segment, index, removeText?: string, removeUsed = true) { From dde033c14a2e3a66ca2fcd86b8f74c836c634f89 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 15:03:44 +0200 Subject: [PATCH 188/878] stackdriver: add alignemnt period --- .../app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index a4f523177a7..f2e6fef79e6 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -21,6 +21,7 @@ export class StackdriverAggregationCtrl { $scope.aggOptions = options.aggOptions; this.setAggOptions(); this.setAlignOptions(); + $scope.alignmentPeriods = options.alignmentPeriods; $scope.onAlignmentChange = this.onAlignmentChange.bind(this); $scope.onAggregationChange = this.onAggregationChange.bind(this); $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); From 70c3e1f3bcae133e05463cbdaeb71b4dd1206d3d Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 26 Sep 2018 15:12:06 +0200 Subject: [PATCH 189/878] tests --- .../features/plugins/PluginActionBar.test.tsx | 30 +++ .../app/features/plugins/PluginList.test.tsx | 24 ++ .../features/plugins/PluginListItem.test.tsx | 33 +++ .../app/features/plugins/PluginListPage.tsx | 2 +- .../features/plugins/__mocks__/pluginMocks.ts | 59 +++++ .../PluginActionBar.test.tsx.snap | 40 ++++ .../__snapshots__/PluginList.test.tsx.snap | 210 ++++++++++++++++++ .../PluginListItem.test.tsx.snap | 106 +++++++++ .../PluginListPage.test.tsx.snap | 5 +- .../features/plugins/state/selectors.test.ts | 31 +++ .../app/features/plugins/state/selectors.ts | 2 +- 11 files changed, 536 insertions(+), 6 deletions(-) create mode 100644 public/app/features/plugins/PluginActionBar.test.tsx create mode 100644 public/app/features/plugins/PluginList.test.tsx create mode 100644 public/app/features/plugins/PluginListItem.test.tsx create mode 100644 public/app/features/plugins/__mocks__/pluginMocks.ts create mode 100644 public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap create mode 100644 public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap create mode 100644 public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap create mode 100644 public/app/features/plugins/state/selectors.test.ts diff --git a/public/app/features/plugins/PluginActionBar.test.tsx b/public/app/features/plugins/PluginActionBar.test.tsx new file mode 100644 index 00000000000..c761b37d9ea --- /dev/null +++ b/public/app/features/plugins/PluginActionBar.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { PluginActionBar, Props } from './PluginActionBar'; + +const setup = (propOverrides?: object) => { + const props: Props = { + searchQuery: '', + layoutMode: 'grid', + setLayoutMode: jest.fn(), + setPluginsSearchQuery: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as PluginActionBar; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginList.test.tsx b/public/app/features/plugins/PluginList.test.tsx new file mode 100644 index 00000000000..74f3ef441eb --- /dev/null +++ b/public/app/features/plugins/PluginList.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PluginList from './PluginList'; +import { getMockPlugins } from './__mocks__/pluginMocks'; + +const setup = (propOverrides?: object) => { + const props = Object.assign( + { + plugins: getMockPlugins(5), + layout: 'grid', + }, + propOverrides + ); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginListItem.test.tsx b/public/app/features/plugins/PluginListItem.test.tsx new file mode 100644 index 00000000000..175911c5e05 --- /dev/null +++ b/public/app/features/plugins/PluginListItem.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PluginListItem from './PluginListItem'; +import { getMockPlugin } from './__mocks__/pluginMocks'; + +const setup = (propOverrides?: object) => { + const props = Object.assign( + { + plugin: getMockPlugin(), + }, + propOverrides + ); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render has plugin section', () => { + const mockPlugin = getMockPlugin(); + mockPlugin.hasUpdate = true; + const wrapper = setup({ + plugin: mockPlugin, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 358eb4a7887..5a467881d94 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -32,7 +32,7 @@ export class PluginListPage extends PureComponent {
    - {}} /> + {plugins && }
    diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts new file mode 100644 index 00000000000..d8dd67d5b61 --- /dev/null +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -0,0 +1,59 @@ +import { Plugin } from 'app/types'; + +export const getMockPlugins = (amount: number): Plugin[] => { + const plugins = []; + + for (let i = 0; i <= amount; i++) { + plugins.push({ + defaultNavUrl: 'some/url', + enabled: false, + hasUpdate: false, + id: `${i}`, + info: { + author: { + name: 'Grafana Labs', + url: 'url/to/GrafanaLabs', + }, + description: 'pretty decent plugin', + links: ['one link'], + logos: { small: 'small/logo', large: 'large/logo' }, + screenshots: `screenshot/${i}`, + updated: '2018-09-26', + version: '1', + }, + latestVersion: `1.${i}`, + name: `pretty cool plugin-${i}`, + pinned: false, + state: '', + type: '', + }); + } + + return plugins; +}; + +export const getMockPlugin = () => { + return { + defaultNavUrl: 'some/url', + enabled: false, + hasUpdate: false, + id: '1', + info: { + author: { + name: 'Grafana Labs', + url: 'url/to/GrafanaLabs', + }, + description: 'pretty decent plugin', + links: ['one link'], + logos: { small: 'small/logo', large: 'large/logo' }, + screenshots: 'screenshot/1', + updated: '2018-09-26', + version: '1', + }, + latestVersion: '1', + name: 'pretty cool plugin 1', + pinned: false, + state: '', + type: '', + }; +}; diff --git a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap new file mode 100644 index 00000000000..30cb53cea27 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    + + +
    + +`; diff --git a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap new file mode 100644 index 00000000000..176304b7b11 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
      + + + + + + +
    +
    +`; diff --git a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap new file mode 100644 index 00000000000..fc0cc68c522 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
  • + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + pretty cool plugin 1 +
    +
    + By Grafana Labs +
    +
    +
    +
    +
  • +`; + +exports[`Render should render has plugin section 1`] = ` +
  • + +
    +
    + +
    +
    + + Update available! + +
    +
    +
    +
    + +
    +
    +
    + pretty cool plugin 1 +
    +
    + By Grafana Labs +
    +
    +
    +
    +
  • +`; diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap index cb8c79bbcee..9c428b54ca0 100644 --- a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -8,10 +8,7 @@ exports[`Render should render component 1`] = `
    - + { + const mockState = initialState; + + it('should return search query', () => { + mockState.searchQuery = 'test'; + const query = getPluginsSearchQuery(mockState); + + expect(query).toEqual(mockState.searchQuery); + }); + + it('should return plugins', () => { + mockState.plugins = getMockPlugins(5); + mockState.searchQuery = ''; + + const plugins = getPlugins(mockState); + + expect(plugins).toEqual(mockState.plugins); + }); + + it('should filter plugins', () => { + mockState.searchQuery = 'plugin-1'; + + const plugins = getPlugins(mockState); + + expect(plugins.length).toEqual(1); + }); +}); diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts index 80c74649a4b..e1d16462527 100644 --- a/public/app/features/plugins/state/selectors.ts +++ b/public/app/features/plugins/state/selectors.ts @@ -2,7 +2,7 @@ export const getPlugins = state => { const regex = new RegExp(state.searchQuery, 'i'); return state.plugins.filter(item => { - return regex.test(item.name); + return regex.test(item.name) || regex.test(item.info.author.name) || regex.test(item.info.description); }); }; From 3590ca2632b92facf5119db49f918c783adf82e7 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 26 Sep 2018 15:18:46 +0200 Subject: [PATCH 190/878] Added constant --- public/app/features/explore/Graph.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 29167dcc4c4..89e48b9d5d1 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -9,6 +9,8 @@ import TimeSeries from 'app/core/time_series2'; import Legend from './Legend'; +const MAX_NUMBER_OF_TIME_SERIES = 20; + // Copied from graph.ts function time_format(ticks, min, max) { if (min && max && ticks) { @@ -74,7 +76,7 @@ class Graph extends Component { getGraphData() { const { data } = this.props; - return this.state.showAllTimeSeries ? data : data.slice(0, 20); + return this.state.showAllTimeSeries ? data : data.slice(0, MAX_NUMBER_OF_TIME_SERIES); } componentDidMount() { From cb90b638d78e19e0a6a31f0f63c02e95256adaa8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 15:22:07 +0200 Subject: [PATCH 191/878] stackdriver: remove montly from alignment periods --- public/app/plugins/datasource/stackdriver/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/constants.ts b/public/app/plugins/datasource/stackdriver/constants.ts index f0d6706c1bd..272b03af218 100644 --- a/public/app/plugins/datasource/stackdriver/constants.ts +++ b/public/app/plugins/datasource/stackdriver/constants.ts @@ -251,5 +251,4 @@ export const alignmentPeriods = [ { text: '6h', value: '+21600s' }, { text: '1d', value: '+86400s' }, { text: '1w', value: '+604800s' }, - { text: '1m', value: '+18748800s' }, ]; From 13c68e6ed833311e310b35ffd0d922d86326bfb2 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 15:37:29 +0200 Subject: [PATCH 192/878] stackdriver: use correct name for variable --- pkg/tsdb/stackdriver/stackdriver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 3a903a544be..6d8c76d5ba1 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -190,8 +190,8 @@ func setAggParams(params *url.Values, query *tsdb.Query) { } re := regexp.MustCompile("[0-9]+") - aa, err := strconv.ParseInt(re.FindString(alignmentPeriod), 10, 64) - if err != nil || aa > 3600 { + seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod), 10, 64) + if err != nil || seconds > 3600 { alignmentPeriod = "+3600s" } From af9033f3e00f1598cc6fcf61213ecf4b7576ab5d Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 15:44:09 +0200 Subject: [PATCH 193/878] stackdriver: distinct grafana auto from stackdriver auto in alignment period --- pkg/tsdb/stackdriver/stackdriver.go | 2 +- pkg/tsdb/stackdriver/stackdriver_test.go | 6 +++--- public/app/plugins/datasource/stackdriver/constants.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 6d8c76d5ba1..5e65e0ae527 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -184,7 +184,7 @@ func setAggParams(params *url.Values, query *tsdb.Query) { perSeriesAligner = "ALIGN_MEAN" } - if alignmentPeriod == "auto" || alignmentPeriod == "" { + if alignmentPeriod == "grafana-auto" || alignmentPeriod == "" { alignmentPeriodValue := int(math.Max(float64(query.IntervalMs), 60.0)) alignmentPeriod = "+" + strconv.Itoa(alignmentPeriodValue) + "s" } diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 9ec47fee4ea..f8b56fdff07 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -67,12 +67,12 @@ func TestStackdriver(t *testing.T) { So(queries[0].Params["filter"][0], ShouldEqual, `metric.type="a/metric/type" key="value" key2="value2"`) }) - Convey("and alignmentPeriod is set to auto", func() { + Convey("and alignmentPeriod is set to grafana-auto", func() { Convey("and IntervalMs is larger than 60", func() { tsdbQuery.Queries[0].IntervalMs = 1000 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "target": "target", - "alignmentPeriod": "auto", + "alignmentPeriod": "grafana-auto", "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"}, }) @@ -84,7 +84,7 @@ func TestStackdriver(t *testing.T) { tsdbQuery.Queries[0].IntervalMs = 30 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "target": "target", - "alignmentPeriod": "auto", + "alignmentPeriod": "grafana-auto", "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"}, }) diff --git a/public/app/plugins/datasource/stackdriver/constants.ts b/public/app/plugins/datasource/stackdriver/constants.ts index 272b03af218..64674a5eb3f 100644 --- a/public/app/plugins/datasource/stackdriver/constants.ts +++ b/public/app/plugins/datasource/stackdriver/constants.ts @@ -243,7 +243,8 @@ export const aggOptions = [ ]; export const alignmentPeriods = [ - { text: 'auto', value: 'auto' }, + { text: 'grafana auto', value: 'grafana-auto' }, + { text: 'stackdriver auto', value: 'stackdriver-auto' }, { text: '1m', value: '+60s' }, { text: '5m', value: '+300s' }, { text: '30m', value: '+1800s' }, From ed6d3bf6ed16f880eff8f60c8016ec557da1a3dc Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 17:50:08 +0200 Subject: [PATCH 194/878] stackdriver: WIP - implement stackdriver style auto alignment period. also return the used alignment period and display it in the query editor --- pkg/tsdb/stackdriver/stackdriver.go | 28 +++++++++++++++++-- .../partials/query.aggregation.html | 16 +++++++---- .../stackdriver/partials/query.editor.html | 2 +- .../stackdriver/query_aggregation_ctrl.ts | 11 ++++++++ .../datasource/stackdriver/query_ctrl.ts | 1 + 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 5e65e0ae527..e6f9ddf6c56 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -116,6 +116,8 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd return nil, err } + durationSeconds := int(endTime.Sub(startTime).Seconds()) + for _, query := range tsdbQuery.Queries { var target string @@ -145,7 +147,7 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339)) params.Add("filter", strings.Trim(fmt.Sprintf(`metric.type="%s" %s`, metricType, filterString), " ")) params.Add("view", query.Model.Get("view").MustString()) - setAggParams(¶ms, query) + setAggParams(¶ms, query, durationSeconds) if setting.Env == setting.DEV { slog.Debug("Stackdriver request", "params", params) @@ -171,7 +173,7 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd return stackdriverQueries, nil } -func setAggParams(params *url.Values, query *tsdb.Query) { +func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { primaryAggregation := query.Model.Get("primaryAggregation").MustString() perSeriesAligner := query.Model.Get("perSeriesAligner").MustString() alignmentPeriod := query.Model.Get("alignmentPeriod").MustString() @@ -185,10 +187,21 @@ func setAggParams(params *url.Values, query *tsdb.Query) { } if alignmentPeriod == "grafana-auto" || alignmentPeriod == "" { - alignmentPeriodValue := int(math.Max(float64(query.IntervalMs), 60.0)) + alignmentPeriodValue := int(math.Max(float64(query.IntervalMs)/1000, 60.0)) alignmentPeriod = "+" + strconv.Itoa(alignmentPeriodValue) + "s" } + if alignmentPeriod == "stackdriver-auto" { + alignmentPeriodValue := int(math.Max(float64(durationSeconds), 60.0)) + if alignmentPeriodValue <= 60*60*5 { + alignmentPeriod = "+60s" + } else if alignmentPeriodValue <= 60*60*23 { + alignmentPeriod = "+300s" + } else { + alignmentPeriod = "+3600s" + } + } + re := regexp.MustCompile("[0-9]+") seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod), 10, 64) if err != nil || seconds > 3600 { @@ -218,6 +231,15 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv req.URL.RawQuery = query.Params.Encode() queryResult.Meta.Set("rawQuery", req.URL.RawQuery) + alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"] + + if ok { + re := regexp.MustCompile("[0-9]+") + seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod[0]), 10, 64) + if err == nil { + queryResult.Meta.Set("alignmentPeriod", seconds) + } + } span, ctx := opentracing.StartSpanFromContext(ctx, "stackdriver query") span.SetTag("target", query.Target) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html index d0eb33f649c..1f1386741e3 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html @@ -28,15 +28,19 @@
    -
    - +
    +
    +
    +
    -
    +
    -
    -
    -
    +
    +
    \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 6f3fb216d24..48ef31c2b43 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -41,7 +41,7 @@
    - +
    Alias By diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index f2e6fef79e6..30dd4fa5c54 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,6 +1,8 @@ import angular from 'angular'; import _ from 'lodash'; import * as options from './constants'; +import * as options from './constants'; +import kbn from 'app/core/utils/kbn'; export class StackdriverAggregation { constructor() { @@ -10,6 +12,7 @@ export class StackdriverAggregation { restrict: 'E', scope: { target: '=', + alignmentPeriod: '<', refresh: '&', }, }; @@ -24,6 +27,7 @@ export class StackdriverAggregationCtrl { $scope.alignmentPeriods = options.alignmentPeriods; $scope.onAlignmentChange = this.onAlignmentChange.bind(this); $scope.onAggregationChange = this.onAggregationChange.bind(this); + $scope.formatAlignmentText = this.formatAlignmentText.bind(this); $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); } @@ -77,6 +81,13 @@ export class StackdriverAggregationCtrl { this.$scope.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; } } + + formatAlignmentText() { + const selectedAlignment = this.$scope.alignOptions.find( + ap => ap.value === this.$scope.target.aggregation.perSeriesAligner + ); + return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`; + } } angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index b9eefcf3789..6ef8bb8ea0e 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -5,6 +5,7 @@ import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; import './query_aggregation_ctrl'; export interface QueryMeta { + alignmentPeriod: string; rawQuery: string; rawQueryString: string; metricLabels: { [key: string]: string[] }; From 4c695dbd57377c6afc785f629b9f4444fc811ec2 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 26 Sep 2018 17:53:33 +0200 Subject: [PATCH 195/878] stackdriver: docs update --- .../features/datasources/stackdriver.md | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 23728def6cd..ef85de435c7 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -12,7 +12,7 @@ weight = 11 # Using Google Stackdriver in Grafana -Grafana ships with built-in support for Google Stackdriver. You just have to add it as a datasource and you will be ready to build dashboards for your Stackdriver metrics. +Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics. It is only available in Grafana 5.3+. The datasource is currently a beta feature and is subject to change. ## Adding the data source to Grafana @@ -20,7 +20,7 @@ Grafana ships with built-in support for Google Stackdriver. You just have to add 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. 3. Click the `+ Add data source` button in the top header. 4. Select `Stackdriver` from the *Type* dropdown. -5. Upload or paste in the Service Account Key (JWT) file. See below for steps to create one. +5. Upload or paste in the Service Account Key file. See below for steps on how to create a Service Account Key file. > NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization. @@ -28,7 +28,7 @@ Name | Description ------------ | ------------- *Name* | The datasource name. This is how you refer to the datasource in panels & queries. *Default* | Default datasource means that it will be pre-selected for new panels. -*Service Account Key* | Service Account File for a GCP Project. Instructions below on how to create it. +*Service Account Key* | Service Account Key File for a GCP Project. Instructions below on how to create it. ## Authentication @@ -77,22 +77,48 @@ The aggregation field lets you combine time series based on common statistics. R The `Aligner` field allows you to align multiple time series after the same group by time interval. Read more about how it works [here](https://cloud.google.com/monitoring/charts/metrics-selector#alignment). +#### Alignment Period/Group by Time + +The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Stackdriver default groupings (which allows you to compare graphs in Grafana with graphs in the Stackdriver UI). +The option is called `Stackdriver auto` and the defaults are: + +- 1m for time ranges < 5 hours +- 5m for time ranges > 5 hours and < 23 hours +- 1h for time ranges > 23 hours + +The other automatic option is `Grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the graph panel. Read more about the details [here](http://docs.grafana.org/reference/templating/#the-interval-variable). + +It is also possible to choose fixed time intervals to group by, like `1h` or `1d`. + ### Group By -Group by resource or metric labels to reduce the number of time series. +Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group by. E.g. Group by instance_name to see an aggregated metric for a Compute instance. ### Alias Patterns -The Alias field allows you to control the format of the metric names in the legend. The default is to show the metric name, labels and the resource. This can be long and hard to read. Using the following patterns in the alias field, you can format the metric name in the legend the way you want it. +The Alias By field allows you to control the format of the legend keys. The default is to show the metric name and labels. This can be long and hard to read. Using the following patterns in the alias field, you can format the legend key the way you want it. -Example Pattern: `{{metric.type}} - {{metric.labels.instance_name}}` +#### Metric Type Patterns + +Alias Pattern | Description | Example Result +----------------- | ---------------------------- | ------------- +`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` +`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` +`{{metric.service}}` | returns the service part | `compute` + +#### Label Patterns + +In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns. + +Alias Pattern Format | Description | Alias Pattern Example | Example Result +---------------------- | ---------------------------------- | ---------------------------- | ------------- +`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` +`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` + +Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}` Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod` -### Table Format / Raw Data - -Change the option `Format As` to `Table` if you want to show raw data in the `Table` panel. - ## Templating Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. @@ -104,13 +130,7 @@ types of template variables. ### Query Variable -If you add a template variable of the type `Query`, this allows you to query Stackdriver for things like metric names and filter keys. The Stackdriver datasource provides the following functions you can use in the `Query` input field: - -Name | Description ----- | -------- -*metrics(project_id, filter expression)* | Returns a list of metrics matching the filter expression. -*label_values(project_id, path to label name, filter expression)* | Returns a list of label values matching the filter expression. -*groups(project_id)* | Returns a list of groups. +Writing variable queries is not supported yet. ### Using variables in queries From c3780d09d4705ec029e8fe11b46ba7425e1f76f3 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 17:56:45 +0200 Subject: [PATCH 196/878] stackdriver: workaround for the fact the jest definitions does not include not --- .../stackdriver/specs/query_aggregation_ctrl.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index af8d8aee3c7..efc935dd338 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -16,7 +16,7 @@ describe('StackdriverAggregationCtrl', () => { ctrl.setAggOptions(); expect(ctrl.$scope.aggOptions.length).toBe(11); expect(ctrl.$scope.aggOptions.map(o => o.value)).toEqual( - expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) + expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); @@ -24,7 +24,7 @@ describe('StackdriverAggregationCtrl', () => { ctrl.setAlignOptions(); expect(ctrl.$scope.alignOptions.length).toBe(10); expect(ctrl.$scope.alignOptions.map(o => o.value)).toEqual( - expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) + expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); }); From 75a2ea5c71e3fd468e6527823a793f4d5a2334d8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Sep 2018 18:04:04 +0200 Subject: [PATCH 197/878] stackdriver: fix broken test --- pkg/tsdb/stackdriver/stackdriver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index f8b56fdff07..28aeef2b153 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -69,7 +69,7 @@ func TestStackdriver(t *testing.T) { Convey("and alignmentPeriod is set to grafana-auto", func() { Convey("and IntervalMs is larger than 60", func() { - tsdbQuery.Queries[0].IntervalMs = 1000 + tsdbQuery.Queries[0].IntervalMs = 1000000 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "target": "target", "alignmentPeriod": "grafana-auto", From 32389f6171336022e2699b903737644fca4f305e Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 26 Sep 2018 21:27:14 +0200 Subject: [PATCH 198/878] using more variables --- public/sass/pages/_explore.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index ac8c8aba554..02a2e75e246 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -57,15 +57,15 @@ .time-series-disclaimer { width: 300px; - margin: 10px auto; + margin: $panel-margin auto; padding: 10px 0; - border-radius: 4px; + border-radius: $border-radius; text-align: center; background-color: $panel-bg; .disclaimer-icon { color: $yellow; - margin-right: 5px; + margin-right: $panel-margin/2; } .show-all-time-series { From 4c4e5533a155ec4b98dc7cbcba1834ca26910cac Mon Sep 17 00:00:00 2001 From: Jiang Ye Date: Sat, 9 Jun 2018 12:16:15 -0400 Subject: [PATCH 199/878] prevent refresh on fixed time window --- public/app/features/dashboard/time_srv.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index dd5a0ba758f..528b7b1de26 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -85,6 +85,11 @@ export class TimeSrv { if (params.to) { this.time.to = this.parseUrlParam(params.to) || this.time.to; } + // if absolute ignore refresh option saved to dashboard + if (params.to && params.to.indexOf('now') === -1) { + this.refresh = false; + } + // but if refresh explicitly set then use that if (params.refresh) { this.refresh = params.refresh || this.refresh; } From 0f4904038e648e2389beaeb6949380f19d82c502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Sep 2018 13:06:36 -0700 Subject: [PATCH 200/878] simplified fix for 12030 --- .../features/dashboard/specs/time_srv.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/app/features/dashboard/specs/time_srv.test.ts b/public/app/features/dashboard/specs/time_srv.test.ts index 514e0b90792..db0d11f2ebe 100644 --- a/public/app/features/dashboard/specs/time_srv.test.ts +++ b/public/app/features/dashboard/specs/time_srv.test.ts @@ -29,6 +29,7 @@ describe('timeSrv', () => { beforeEach(() => { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); + _dashboard.refresh = false; }); describe('timeRange', () => { @@ -79,6 +80,23 @@ describe('timeSrv', () => { expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime()); }); + it('should ignore refresh if time absolute', () => { + location = { + search: jest.fn(() => ({ + from: '20140410T052010', + to: '20140520T031022', + })), + }; + + timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + + // dashboard saved with refresh on + _dashboard.refresh = true; + timeSrv.init(_dashboard); + + expect(timeSrv.refresh).toBe(false); + }); + it('should handle formatted dates without time', () => { location = { search: jest.fn(() => ({ From 283f6936008454b7d0636b76df7fb1f51ab70594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Sep 2018 13:13:00 -0700 Subject: [PATCH 201/878] fix: also set dashboard refresh to false --- public/app/features/dashboard/time_srv.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 528b7b1de26..5bf23c66bab 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -88,6 +88,7 @@ export class TimeSrv { // if absolute ignore refresh option saved to dashboard if (params.to && params.to.indexOf('now') === -1) { this.refresh = false; + this.dashboard.refresh = false; } // but if refresh explicitly set then use that if (params.refresh) { From 803e716213776c1cd8020efd35a9a2f753d3d680 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Wed, 26 Sep 2018 22:34:04 +0200 Subject: [PATCH 202/878] Add goconst to CircleCI --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e631e0a8d33..2225e0a16cf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,13 +83,14 @@ jobs: - checkout - run: 'go get -u github.com/alecthomas/gometalinter' - run: 'go get -u github.com/tsenart/deadcode' + - run: 'go get -u github.com/jgautheron/goconst/cmd/goconst' - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' - run: 'go get -u github.com/mdempsky/unconvert' - run: 'go get -u github.com/opennota/check/cmd/varcheck' - run: name: run linters - command: 'gometalinter --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=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' - run: name: run go vet command: 'go vet ./pkg/...' From dbec2ded253b4bffb828b53aaedf24293f37aba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Sep 2018 09:15:23 +0200 Subject: [PATCH 203/878] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39479054af3..42951d5dbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * **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) * **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon) +* **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030) # 5.3.0 (unreleased) From 3c6c456592c2fa68abd9f12a294cb530664e578e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 09:19:24 +0200 Subject: [PATCH 204/878] stackdriver: use more appropriate test data --- pkg/tsdb/stackdriver/stackdriver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 28aeef2b153..f51697b17b5 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -68,7 +68,7 @@ func TestStackdriver(t *testing.T) { }) Convey("and alignmentPeriod is set to grafana-auto", func() { - Convey("and IntervalMs is larger than 60", func() { + Convey("and IntervalMs is larger than 60000", func() { tsdbQuery.Queries[0].IntervalMs = 1000000 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "target": "target", @@ -80,8 +80,8 @@ func TestStackdriver(t *testing.T) { So(err, ShouldBeNil) So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+1000s`) }) - Convey("and IntervalMs is less than 60", func() { - tsdbQuery.Queries[0].IntervalMs = 30 + Convey("and IntervalMs is less than 60000", func() { + tsdbQuery.Queries[0].IntervalMs = 30000 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "target": "target", "alignmentPeriod": "grafana-auto", From f6b8d3a1c2e1aa2fb80bf7022f3275018be38209 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 27 Sep 2018 09:24:40 +0200 Subject: [PATCH 205/878] devenv: grafana high availability (ha) test setup --- devenv/docker/ha_test/.gitignore | 1 + devenv/docker/ha_test/README.md | 137 ++++++++++++ devenv/docker/ha_test/alerts.sh | 156 ++++++++++++++ devenv/docker/ha_test/docker-compose.yaml | 57 +++++ .../grafana/provisioning/alerts.jsonnet | 202 ++++++++++++++++++ .../provisioning/dashboards/alerts.yaml | 8 + .../dashboards/alerts/overview.json | 172 +++++++++++++++ .../provisioning/datasources/datasources.yaml | 11 + .../docker/ha_test/prometheus/prometheus.yml | 39 ++++ 9 files changed, 783 insertions(+) create mode 100644 devenv/docker/ha_test/.gitignore create mode 100644 devenv/docker/ha_test/README.md create mode 100755 devenv/docker/ha_test/alerts.sh create mode 100644 devenv/docker/ha_test/docker-compose.yaml create mode 100644 devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet create mode 100644 devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml create mode 100644 devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json create mode 100644 devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml create mode 100644 devenv/docker/ha_test/prometheus/prometheus.yml diff --git a/devenv/docker/ha_test/.gitignore b/devenv/docker/ha_test/.gitignore new file mode 100644 index 00000000000..0f4e139e204 --- /dev/null +++ b/devenv/docker/ha_test/.gitignore @@ -0,0 +1 @@ +grafana/provisioning/dashboards/alerts/alert-* \ No newline at end of file diff --git a/devenv/docker/ha_test/README.md b/devenv/docker/ha_test/README.md new file mode 100644 index 00000000000..bc93727ceae --- /dev/null +++ b/devenv/docker/ha_test/README.md @@ -0,0 +1,137 @@ +# Grafana High Availability (HA) test setup + +A set of docker compose services which together creates a Grafana HA test setup with capability of easily +scaling up/down number of Grafana instances. + +Included services + +* Grafana +* Mysql - Grafana configuration database and session storage +* Prometheus - Monitoring of Grafana and used as datasource of provisioned alert rules +* Nginx - Reverse proxy for Grafana and Prometheus. Enables browsing Grafana/Prometheus UI using a hostname + +## Prerequisites + +### Build grafana docker container + +Build a Grafana docker container from current branch and commit and tag it as grafana/grafana:dev. + +```bash +$ cd +$ make build-docker-full +``` + +### Virtual host names + +#### Alternative 1 - Use dnsmasq + +```bash +$ sudo apt-get install dnsmasq +$ echo 'address=/loc/127.0.0.1' | sudo tee /etc/dnsmasq.d/dnsmasq-loc.conf > /dev/null +$ sudo /etc/init.d/dnsmasq restart +$ ping whatever.loc +PING whatever.loc (127.0.0.1) 56(84) bytes of data. +64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.076 ms +--- whatever.loc ping statistics --- +1 packet transmitted, 1 received, 0% packet loss, time 1998ms +``` + +#### Alternative 2 - Manually update /etc/hosts + +Update your `/etc/hosts` to be able to access Grafana and/or Prometheus UI using a hostname. + +```bash +$ cat /etc/hosts +127.0.0.1 grafana.loc +127.0.0.1 prometheus.loc +``` + +## Start services + +```bash +$ docker-compose up -d +``` + +Browse +* http://grafana.loc/ +* http://prometheus.loc/ + +Check for any errors + +```bash +$ docker-compose logs | grep error +``` + +### Scale Grafana instances up/down + +Scale number of Grafana instances to `` + +```bash +$ docker-compose up --scale grafana= -d +# for example 3 instances +$ docker-compose up --scale grafana=3 -d +``` + +## Test alerting + +### Create notification channels + +Creates default notification channels, if not already exists + +```bash +$ ./alerts.sh setup +``` + +### Slack notifications + +Disable + +```bash +$ ./alerts.sh slack -d +``` + +Enable and configure url + +```bash +$ ./alerts.sh slack -u https://hooks.slack.com/services/... +``` + +Enable, configure url and enable reminders + +```bash +$ ./alerts.sh slack -u https://hooks.slack.com/services/... -r -e 10m +``` + +### Provision alert dashboards with alert rules + +Provision 1 dashboard/alert rule (default) + +```bash +$ ./alerts.sh provision +``` + +Provision 10 dashboards/alert rules + +```bash +$ ./alerts.sh provision -a 10 +``` + +Provision 10 dashboards/alert rules and change condition to `gt > 100` + +```bash +$ ./alerts.sh provision -a 10 -c 100 +``` + +### Pause/unpause all alert rules + +Pause + +```bash +$ ./alerts.sh pause +``` + +Unpause + +```bash +$ ./alerts.sh unpause +``` diff --git a/devenv/docker/ha_test/alerts.sh b/devenv/docker/ha_test/alerts.sh new file mode 100755 index 00000000000..a05a4581739 --- /dev/null +++ b/devenv/docker/ha_test/alerts.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +requiresJsonnet() { + if ! type "jsonnet" > /dev/null; then + echo "you need you install jsonnet to run this script" + echo "follow the instructions on https://github.com/google/jsonnet" + exit 1 + fi +} + +setup() { + STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/1) + if [ $STATUS -eq 200 ]; then + echo "Email already exists, skipping..." + else + curl -H "Content-Type: application/json" \ + -d '{ + "name": "Email", + "type": "email", + "isDefault": false, + "sendReminder": false, + "uploadImage": true, + "settings": { + "addresses": "user@test.com" + } + }' \ + http://admin:admin@grafana.loc/api/alert-notifications + fi + + STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/2) + if [ $STATUS -eq 200 ]; then + echo "Slack already exists, skipping..." + else + curl -H "Content-Type: application/json" \ + -d '{ + "name": "Slack", + "type": "slack", + "isDefault": false, + "sendReminder": false, + "uploadImage": true + }' \ + http://admin:admin@grafana.loc/api/alert-notifications + fi +} + +slack() { + enabled=true + url='' + remind=false + remindEvery='10m' + + while getopts ":e:u:dr" o; do + case "${o}" in + e) + remindEvery=${OPTARG} + ;; + u) + url=${OPTARG} + ;; + d) + enabled=false + ;; + r) + remind=true + ;; + esac + done + shift $((OPTIND-1)) + + curl -X PUT \ + -H "Content-Type: application/json" \ + -d '{ + "id": 2, + "name": "Slack", + "type": "slack", + "isDefault": '$enabled', + "sendReminder": '$remind', + "frequency": "'$remindEvery'", + "uploadImage": true, + "settings": { + "url": "'$url'" + } + }' \ + http://admin:admin@grafana.loc/api/alert-notifications/2 +} + +provision() { + alerts=1 + condition=65 + while getopts ":a:c:" o; do + case "${o}" in + a) + alerts=${OPTARG} + ;; + c) + condition=${OPTARG} + ;; + esac + done + shift $((OPTIND-1)) + + requiresJsonnet + + rm -rf grafana/provisioning/dashboards/alerts/alert-*.json + jsonnet -m grafana/provisioning/dashboards/alerts grafana/provisioning/alerts.jsonnet --ext-code alerts=$alerts --ext-code condition=$condition +} + +pause() { + curl -H "Content-Type: application/json" \ + -d '{"paused":true}' \ + http://admin:admin@grafana.loc/api/admin/pause-all-alerts +} + +unpause() { + curl -H "Content-Type: application/json" \ + -d '{"paused":false}' \ + http://admin:admin@grafana.loc/api/admin/pause-all-alerts +} + +usage() { + echo -e "Usage: ./alerts.sh COMMAND [OPTIONS]\n" + echo -e "Commands" + echo -e " setup\t\t creates default alert notification channels" + echo -e " slack\t\t configure slack notification channel" + echo -e " [-d]\t\t\t disable notifier, default enabled" + echo -e " [-u]\t\t\t url" + echo -e " [-r]\t\t\t send reminders" + echo -e " [-e ]\t\t default 10m\n" + echo -e " provision\t provision alerts" + echo -e " [-a ]\t default 1" + echo -e " [-c ]\t default 65\n" + echo -e " pause\t\t pause all alerts" + echo -e " unpause\t unpause all alerts" +} + +main() { + local cmd=$1 + + if [[ $cmd == "setup" ]]; then + setup + elif [[ $cmd == "slack" ]]; then + slack "${@:2}" + elif [[ $cmd == "provision" ]]; then + provision "${@:2}" + elif [[ $cmd == "pause" ]]; then + pause + elif [[ $cmd == "unpause" ]]; then + unpause + fi + + if [[ -z "$cmd" ]]; then + usage + fi +} + +main "$@" diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml new file mode 100644 index 00000000000..78f98ab8dc5 --- /dev/null +++ b/devenv/docker/ha_test/docker-compose.yaml @@ -0,0 +1,57 @@ +version: "2.1" + +services: + nginx-proxy: + image: jwilder/nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + + mysql: + image: mysql + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: grafana + MYSQL_USER: grafana + MYSQL_PASSWORD: password + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 10s + retries: 10 + + grafana: + image: grafana/grafana:dev + volumes: + - ./grafana/provisioning/:/etc/grafana/provisioning/ + environment: + - VIRTUAL_HOST=grafana.loc + - GF_SERVER_ROOT_URL=http://grafana.loc + - GF_DATABASE_TYPE=mysql + - GF_DATABASE_HOST=mysql:3306 + - GF_DATABASE_NAME=grafana + - GF_DATABASE_USER=grafana + - GF_DATABASE_PASSWORD=password + - GF_SESSION_PROVIDER=mysql + - GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(mysql:3306)/grafana?allowNativePasswords=true + ports: + - 3000 + depends_on: + mysql: + condition: service_healthy + + prometheus: + image: prom/prometheus:v2.4.2 + volumes: + - ./prometheus/:/etc/prometheus/ + environment: + - VIRTUAL_HOST=prometheus.loc + ports: + - 9090 + + # mysqld-exporter: + # image: prom/mysqld-exporter + # environment: + # - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/ + # ports: + # - 9104 \ No newline at end of file diff --git a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet new file mode 100644 index 00000000000..86ded7e79d6 --- /dev/null +++ b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet @@ -0,0 +1,202 @@ +local numAlerts = std.extVar('alerts'); +local condition = std.extVar('condition'); +local arr = std.range(1, numAlerts); + +local alertDashboardTemplate = { + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 65 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "bulk alerting", + "noDataState": "no_data", + "notifications": [ + { + "id": 2 + } + ] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "$$hashKey": "object:117", + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 50 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "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 + } + ] + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "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" + ] + }, + "timezone": "", + "title": "New dashboard", + "uid": null, + "version": 0 +}; + + +{ + ['alert-' + std.toString(x) + '.json']: + alertDashboardTemplate + { + panels: [ + alertDashboardTemplate.panels[0] + + { + alert+: { + name: 'Alert rule ' + x, + conditions: [ + alertDashboardTemplate.panels[0].alert.conditions[0] + + { + evaluator+: { + params: [condition] + } + }, + ], + }, + }, + ], + uid: 'alert-' + x, + title: 'Alert ' + x + }, + for x in arr +} \ No newline at end of file diff --git a/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml new file mode 100644 index 00000000000..60b6cd4bb04 --- /dev/null +++ b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: + - name: 'Alerts' + folder: 'Alerts' + type: file + options: + path: /etc/grafana/provisioning/dashboards/alerts diff --git a/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json new file mode 100644 index 00000000000..53e33c37b1f --- /dev/null +++ b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json @@ -0,0 +1,172 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": { + "Active alerts": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "interval": "", + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Active grafana instances", + "dashes": true, + "fill": 0 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(grafana_alerting_notification_sent_total[1m])) by(job)", + "format": "time_series", + "instant": false, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "Notifications sent", + "refId": "A" + }, + { + "expr": "min(grafana_alerting_active_alerts) without(instance)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "Active alerts", + "refId": "B" + }, + { + "expr": "count(up{job=\"grafana\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Active grafana instances", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Notifications sent vs active alerts", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": 3 + } + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "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" + ] + }, + "timezone": "", + "title": "Overview", + "uid": "xHy7-hAik", + "version": 6 +} \ No newline at end of file diff --git a/devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml b/devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml new file mode 100644 index 00000000000..8d59793be16 --- /dev/null +++ b/devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + jsonData: + timeInterval: 10s + queryTimeout: 30s + httpMethod: POST \ No newline at end of file diff --git a/devenv/docker/ha_test/prometheus/prometheus.yml b/devenv/docker/ha_test/prometheus/prometheus.yml new file mode 100644 index 00000000000..ea97ba8ba05 --- /dev/null +++ b/devenv/docker/ha_test/prometheus/prometheus.yml @@ -0,0 +1,39 @@ +# my global config +global: + scrape_interval: 10s # By default, scrape targets every 15 seconds. + evaluation_interval: 10s # By default, scrape targets every 15 seconds. + # scrape_timeout is set to the global default (10s). + +# Load and evaluate rules in this file every 'evaluation_interval' seconds. +#rule_files: +# - "alert.rules" +# - "first.rules" +# - "second.rules" + +# alerting: +# alertmanagers: +# - scheme: http +# static_configs: +# - targets: +# - "127.0.0.1:9093" + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'grafana' + dns_sd_configs: + - names: + - 'grafana' + type: 'A' + port: 3000 + refresh_interval: 10s + + # - job_name: 'mysql' + # dns_sd_configs: + # - names: + # - 'mysqld-exporter' + # type: 'A' + # port: 9104 + # refresh_interval: 10s \ No newline at end of file From 0476520f2dc3c8e68e58b6921fcff76b3d6b1168 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 27 Sep 2018 09:25:57 +0200 Subject: [PATCH 206/878] changelog: adds note about closing #12534 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42951d5dbbb..7ecc3339600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Minor +* **Provisioning**: Dashboard Provisioning now support symlinks that changes target [#12534](https://github.com/grafana/grafana/issues/12534), thx [@auhlig](https://github.com/auhlig) * **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) From ff79f80685fb4c17b3fadb239e22c018dddc8902 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 26 Sep 2018 17:26:02 +0200 Subject: [PATCH 207/878] initial rename refactoring --- pkg/models/alert_notifications.go | 17 ++++---- pkg/services/alerting/notifier.go | 39 +++++++++---------- pkg/services/alerting/notifiers/base.go | 19 ++++----- pkg/services/alerting/notifiers/base_test.go | 27 ++++++------- pkg/services/alerting/result_handler.go | 13 ------- pkg/services/sqlstore/alert_notification.go | 18 ++------- .../sqlstore/alert_notification_test.go | 33 +++------------- pkg/services/sqlstore/migrations/alert_mig.go | 21 ++++++++++ 8 files changed, 81 insertions(+), 106 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index b90b3d36ced..419f5048af4 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -76,33 +76,36 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } -type AlertNotificationJournal struct { +type AlertNotificationState struct { Id int64 OrgId int64 AlertId int64 NotifierId int64 SentAt int64 - Success bool + State string + Version int64 } -type RecordNotificationJournalCommand struct { +type UpdateAlertNotificationStateCommand struct { OrgId int64 AlertId int64 NotifierId int64 SentAt int64 - Success bool + State bool } -type GetLatestNotificationQuery struct { +type GetNotificationStateQuery struct { OrgId int64 AlertId int64 NotifierId int64 - Result []AlertNotificationJournal + Result *AlertNotificationState } -type CleanNotificationJournalCommand struct { +type InsertAlertNotificationCommand struct { OrgId int64 AlertId int64 NotifierId int64 + SentAt int64 + State string } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index cbad5cbfdcf..ec448a8c1b0 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -4,12 +4,10 @@ import ( "context" "errors" "fmt" - "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -68,30 +66,31 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi // Verify that we can send the notification again // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(ctx, evalContext) { - return nil - } + // if !evalContext.IsTestRun && !not.ShouldNotify(ctx, evalContext) { + // return nil + // } - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + // n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + // metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - //send notification - success := not.Notify(evalContext) == nil + // //send notification + // // success := not.Notify(evalContext) == nil - if evalContext.IsTestRun { - return nil - } + // if evalContext.IsTestRun { + // return nil + // } //write result to db. - cmd := &m.RecordNotificationJournalCommand{ - OrgId: evalContext.Rule.OrgId, - AlertId: evalContext.Rule.Id, - NotifierId: not.GetNotifierId(), - SentAt: time.Now().Unix(), - Success: success, - } + // cmd := &m.RecordNotificationJournalCommand{ + // OrgId: evalContext.Rule.OrgId, + // AlertId: evalContext.Rule.Id, + // NotifierId: not.GetNotifierId(), + // SentAt: time.Now().Unix(), + // Success: success, + // } - return bus.DispatchCtx(ctx, cmd) + // return bus.DispatchCtx(ctx, cmd) + return nil }) if err != nil { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 24daa02bce8..f71a41235f7 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -42,22 +42,23 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { } } -func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, journals []models.AlertNotificationJournal) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, notificationState *models.AlertNotificationState) bool { // Only notify on state change. if context.PrevAlertState == context.Rule.State && !sendReminder { return false } // get last successfully sent notification - lastNotify := time.Time{} - for _, j := range journals { - if j.Success { - lastNotify = time.Unix(j.SentAt, 0) - break - } - } + // lastNotify := time.Time{} + // for _, j := range journals { + // if j.Success { + // lastNotify = time.Unix(j.SentAt, 0) + // break + // } + // } // Do not notify if interval has not elapsed + lastNotify := time.Unix(notificationState.SentAt, 0) if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { return false } @@ -77,7 +78,7 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ // ShouldNotify checks this evaluation should send an alert notification func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool { - cmd := &models.GetLatestNotificationQuery{ + cmd := &models.GetNotificationStateQuery{ OrgId: c.Rule.OrgId, AlertId: c.Rule.Id, NotifierId: n.Id, diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 9ea4b82fd54..c14006637a4 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -23,7 +23,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState m.AlertStateType sendReminder bool frequency time.Duration - journals []m.AlertNotificationJournal + journals *m.AlertNotificationState expect bool }{ @@ -32,7 +32,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStatePending, prevState: m.AlertStateOK, sendReminder: false, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: false, }, @@ -41,7 +41,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateAlerting, sendReminder: false, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: true, }, @@ -50,7 +50,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStatePending, sendReminder: false, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: false, }, @@ -59,7 +59,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: false, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: false, }, @@ -68,7 +68,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateAlerting, sendReminder: true, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: true, }, @@ -77,7 +77,7 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: true, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: false, }, @@ -87,7 +87,7 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - journals: []m.AlertNotificationJournal{}, + journals: &m.AlertNotificationState{}, expect: true, }, @@ -97,9 +97,7 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - journals: []m.AlertNotificationJournal{ - {SentAt: tnow.Add(-time.Minute).Unix(), Success: true}, - }, + journals: &m.AlertNotificationState{SentAt: tnow.Add(-time.Minute).Unix()}, expect: false, }, @@ -110,10 +108,7 @@ func TestShouldSendAlertNotification(t *testing.T) { frequency: time.Minute * 10, sendReminder: true, expect: true, - journals: []m.AlertNotificationJournal{ - {SentAt: tnow.Add(-time.Minute).Unix(), Success: false}, // recent failed notification - {SentAt: tnow.Add(-time.Hour).Unix(), Success: true}, // old successful notification - }, + journals: &m.AlertNotificationState{SentAt: tnow.Add(-time.Hour).Unix()}, }, } @@ -142,7 +137,7 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) Convey("should not notify query returns error", func() { - bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetNotificationStateQuery) error { return errors.New("some kind of error unknown error") }) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 893cca948f9..e2c70de0e28 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,19 +88,6 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } - if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { - for _, notifierId := range evalContext.Rule.Notifications { - cmd := &m.CleanNotificationJournalCommand{ - AlertId: evalContext.Rule.Id, - NotifierId: notifierId, - OrgId: evalContext.Rule.OrgId, - } - if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { - handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) - } - } - } - handler.notifier.SendIfNeeded(evalContext) return nil } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index df247e6891d..ece06614d4a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -20,7 +20,6 @@ func init() { bus.AddHandler("sql", GetAllAlertNotifications) bus.AddHandlerCtx("sql", RecordNotificationJournal) bus.AddHandlerCtx("sql", GetLatestNotification) - bus.AddHandlerCtx("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -229,14 +228,13 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { }) } -func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error { +func RecordNotificationJournal(ctx context.Context, cmd *m.UpdateAlertNotificationStateCommand) error { return withDbSession(ctx, func(sess *DBSession) error { - journalEntry := &m.AlertNotificationJournal{ + journalEntry := &m.AlertNotificationState{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, NotifierId: cmd.NotifierId, SentAt: cmd.SentAt, - Success: cmd.Success, } _, err := sess.Insert(journalEntry) @@ -244,9 +242,9 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou }) } -func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { +func GetLatestNotification(ctx context.Context, cmd *m.GetNotificationStateQuery) error { return withDbSession(ctx, func(sess *DBSession) error { - nj := []m.AlertNotificationJournal{} + nj := &m.AlertNotificationState{} err := sess.Desc("alert_notification_journal.sent_at"). Where("alert_notification_journal.org_id = ?", cmd.OrgId). @@ -262,11 +260,3 @@ func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuer return nil }) } - -func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { - return inTransactionCtx(ctx, func(sess *DBSession) error { - sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" - _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) - return err - }) -} diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 1e3df45b5cf..742d459e1ca 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -20,17 +20,17 @@ func TestAlertNotificationSQLAccess(t *testing.T) { var notifierId int64 = 10 Convey("Getting last journal should raise error if no one exists", func() { - query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - GetLatestNotification(context.Background(), query) - So(len(query.Result), ShouldEqual, 0) + query := &m.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldNotBeNil) // recording an journal entry in another org to make sure org filter works as expected. - journalInOtherOrg := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: 10, Success: true, SentAt: 1} - err := RecordNotificationJournal(context.Background(), journalInOtherOrg) + journalInOtherOrg := &m.UpdateAlertNotificationStateCommand{AlertId: alertId, NotifierId: notifierId, OrgId: 10, SentAt: 1} + err = RecordNotificationJournal(context.Background(), journalInOtherOrg) So(err, ShouldBeNil) Convey("should be able to record two journaling events", func() { - createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1} + createCmd := &m.UpdateAlertNotificationStateCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, SentAt: 1} err := RecordNotificationJournal(context.Background(), createCmd) So(err, ShouldBeNil) @@ -39,27 +39,6 @@ func TestAlertNotificationSQLAccess(t *testing.T) { err = RecordNotificationJournal(context.Background(), createCmd) So(err, ShouldBeNil) - - Convey("get last journaling event", func() { - err := GetLatestNotification(context.Background(), query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - last := query.Result[0] - So(last.SentAt, ShouldEqual, 1001) - - Convey("be able to clear all journaling for an notifier", func() { - cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId} - err := CleanNotificationJournal(context.Background(), cmd) - So(err, ShouldBeNil) - - Convey("querying for last journaling should return no journal entries", func() { - query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - err := GetLatestNotification(context.Background(), query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 0) - }) - }) - }) }) }) }) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index e27e64c6124..d6886f6ecae 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -107,4 +107,25 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) + + mg.AddMigration("drop alert_notification_journal", NewDropTableMigration("alert_notification_journal")) + + alert_notification_state := Table{ + Name: "alert_notification_state", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_BigInt, Nullable: false}, + {Name: "state", Type: DB_NVarchar, Length: 50, Nullable: false}, + {Name: "version", Type: DB_BigInt, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state)) + mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, notification_journal.Indices[0])) } From d405d8f255ac2bd1f2a561a5491f8f5a778ebc23 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 26 Sep 2018 18:19:30 +0200 Subject: [PATCH 208/878] stackdriver: publish docs to v5.3 (not root) --- docs/sources/features/datasources/stackdriver.md | 6 ++++-- docs/versions.json | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index ef85de435c7..934e8ec2378 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -61,9 +61,11 @@ Click on the links above and click the `Enable` button: ![Choose role](/img/docs/v54/stackdriver_service_account_choose_role.png) 5. Click the Create button. A JSON key file will be created and downloaded to your computer. Store this file in a secure place as it allows access to your Stackdriver data. 6. Upload it to Grafana on the datasource Configuration page. You can either upload the file or paste in the contents of the file. - ![Choose role](/img/docs/v54/stackdriver_grafana_upload_key.png) + + ![Choose role](/img/docs/v54/stackdriver_grafana_upload_key.png) 7. The file contents will be encrypted and saved in the Grafana database. Don't forget to save after uploading the file! - ![Choose role](/img/docs/v54/stackdriver_grafana_key_uploaded.png) + + ![Choose role](/img/docs/v54/stackdriver_grafana_key_uploaded.png) ## Metric Query Editor diff --git a/docs/versions.json b/docs/versions.json index caefbe198d6..34e9c2150e1 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,4 +1,5 @@ [ + { "version": "v5.3", "path": "/v5.3", "archived": false, "current": false }, { "version": "v5.2", "path": "/", "archived": false, "current": true }, { "version": "v5.1", "path": "/v5.1", "archived": true }, { "version": "v5.0", "path": "/v5.0", "archived": true }, From 6358d3f314dc35f452a08f7080368350e7e73654 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 27 Sep 2018 10:13:32 +0200 Subject: [PATCH 209/878] stackdriver: set target to be raw query --- pkg/tsdb/stackdriver/stackdriver.go | 51 +++++++++--------------- pkg/tsdb/stackdriver/stackdriver_test.go | 12 ++---- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index e6f9ddf6c56..66691714ce4 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -121,34 +121,18 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd for _, query := range tsdbQuery.Queries { var target string - if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { - target = fixIntervalFormat(fullTarget) - } else { - target = fixIntervalFormat(query.Model.Get("target").MustString()) - } - metricType := query.Model.Get("metricType").MustString() filterParts := query.Model.Get("filters").MustArray() - filterString := "" - for i, part := range filterParts { - mod := i % 4 - if part == "AND" { - filterString += " " - } else if mod == 2 { - filterString += fmt.Sprintf(`"%s"`, part) - } else { - filterString += part.(string) - } - } - params := url.Values{} params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339)) params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339)) - params.Add("filter", strings.Trim(fmt.Sprintf(`metric.type="%s" %s`, metricType, filterString), " ")) + params.Add("filter", buildFilterString(metricType, filterParts)) params.Add("view", query.Model.Get("view").MustString()) setAggParams(¶ms, query, durationSeconds) + target = params.Encode() + if setting.Env == setting.DEV { slog.Debug("Stackdriver request", "params", params) } @@ -173,6 +157,21 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd return stackdriverQueries, nil } +func buildFilterString(metricType string, filterParts []interface{}) string { + filterString := "" + for i, part := range filterParts { + mod := i % 4 + if part == "AND" { + filterString += " " + } else if mod == 2 { + filterString += fmt.Sprintf(`"%s"`, part) + } else { + filterString += part.(string) + } + } + return strings.Trim(fmt.Sprintf(`metric.type="%s" %s`, metricType, filterString), " ") +} + func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { primaryAggregation := query.Model.Get("primaryAggregation").MustString() perSeriesAligner := query.Model.Get("perSeriesAligner").MustString() @@ -457,17 +456,3 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. return req, nil } - -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/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 28aeef2b153..fdd71dd9438 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -27,7 +27,6 @@ func TestStackdriver(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "metricType": "a/metric/type", "view": "FULL", "aliasBy": "testalias", @@ -44,7 +43,7 @@ func TestStackdriver(t *testing.T) { So(len(queries), ShouldEqual, 1) So(queries[0].RefID, ShouldEqual, "A") - So(queries[0].Target, ShouldEqual, "target") + So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_NONE&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL") So(len(queries[0].Params), ShouldEqual, 7) So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z") So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z") @@ -56,7 +55,6 @@ func TestStackdriver(t *testing.T) { Convey("and query has filters", func() { tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "metricType": "a/metric/type", "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"}, }) @@ -71,7 +69,6 @@ func TestStackdriver(t *testing.T) { Convey("and IntervalMs is larger than 60", func() { tsdbQuery.Queries[0].IntervalMs = 1000000 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "alignmentPeriod": "grafana-auto", "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"}, }) @@ -83,7 +80,6 @@ func TestStackdriver(t *testing.T) { Convey("and IntervalMs is less than 60", func() { tsdbQuery.Queries[0].IntervalMs = 30 tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "alignmentPeriod": "grafana-auto", "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"}, }) @@ -120,7 +116,6 @@ func TestStackdriver(t *testing.T) { Convey("and query has aggregation mean set", func() { tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "metricType": "a/metric/type", "primaryAggregation": "REDUCE_MEAN", "view": "FULL", @@ -131,7 +126,7 @@ func TestStackdriver(t *testing.T) { So(len(queries), ShouldEqual, 1) So(queries[0].RefID, ShouldEqual, "A") - So(queries[0].Target, ShouldEqual, "target") + So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_MEAN&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL") So(len(queries[0].Params), ShouldEqual, 7) So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z") So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z") @@ -144,7 +139,6 @@ func TestStackdriver(t *testing.T) { Convey("and query has group bys", func() { tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ - "target": "target", "metricType": "a/metric/type", "primaryAggregation": "REDUCE_NONE", "groupBys": []interface{}{"metric.label.group1", "metric.label.group2"}, @@ -156,7 +150,7 @@ func TestStackdriver(t *testing.T) { So(len(queries), ShouldEqual, 1) So(queries[0].RefID, ShouldEqual, "A") - So(queries[0].Target, ShouldEqual, "target") + So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_NONE&aggregation.groupByFields=metric.label.group1&aggregation.groupByFields=metric.label.group2&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL") So(len(queries[0].Params), ShouldEqual, 8) So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z") So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z") From 481b8653d9eaeee86fe54507bff532574fda81b4 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 10:20:01 +0200 Subject: [PATCH 210/878] stackdriver: update alignment period rules according to stackdriver --- pkg/tsdb/stackdriver/stackdriver.go | 6 ++- pkg/tsdb/stackdriver/stackdriver_test.go | 54 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index e6f9ddf6c56..75f9209b735 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -18,6 +18,7 @@ import ( "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/api/pluginproxy" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -193,9 +194,10 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { if alignmentPeriod == "stackdriver-auto" { alignmentPeriodValue := int(math.Max(float64(durationSeconds), 60.0)) - if alignmentPeriodValue <= 60*60*5 { + logger.Info("alignmentPeriodValue", "alignmentPeriodValue", alignmentPeriodValue) + if alignmentPeriodValue < 60*60*23 { alignmentPeriod = "+60s" - } else if alignmentPeriodValue <= 60*60*23 { + } else if alignmentPeriodValue < 60*60*24*6 { alignmentPeriod = "+300s" } else { alignmentPeriod = "+3600s" diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index f51697b17b5..b8bd760571a 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -94,6 +94,60 @@ func TestStackdriver(t *testing.T) { }) }) + Convey("and alignmentPeriod is set to stackdriver-auto", func() { + Convey("and range is two hours", func() { + tsdbQuery.TimeRange.From = "1538033322461" + tsdbQuery.TimeRange.To = "1538040522461" + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "target": "target", + "alignmentPeriod": "stackdriver-auto", + }) + + queries, err := executor.buildQueries(tsdbQuery) + So(err, ShouldBeNil) + So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`) + }) + + Convey("and range is 22 hours", func() { + tsdbQuery.TimeRange.From = "1538034524922" + tsdbQuery.TimeRange.To = "1538113724922" + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "target": "target", + "alignmentPeriod": "stackdriver-auto", + }) + + queries, err := executor.buildQueries(tsdbQuery) + So(err, ShouldBeNil) + So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`) + }) + + Convey("and range is 23 hours", func() { + tsdbQuery.TimeRange.From = "1538034567985" + tsdbQuery.TimeRange.To = "1538117367985" + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "target": "target", + "alignmentPeriod": "stackdriver-auto", + }) + + queries, err := executor.buildQueries(tsdbQuery) + So(err, ShouldBeNil) + So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+300s`) + }) + + Convey("and range is 7 days", func() { + tsdbQuery.TimeRange.From = "1538036324073" + tsdbQuery.TimeRange.To = "1538641124073" + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "target": "target", + "alignmentPeriod": "stackdriver-auto", + }) + + queries, err := executor.buildQueries(tsdbQuery) + So(err, ShouldBeNil) + So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+3600s`) + }) + }) + Convey("and alignmentPeriod is set in frontend", func() { Convey("and alignment period is too big", func() { tsdbQuery.Queries[0].IntervalMs = 1000 From c14c848819a1841960adfa172aa52ba9ec811274 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 10:23:37 +0200 Subject: [PATCH 211/878] stackdriver: update docs so that they align with alignment period rules in stackdriver gui --- .../features/datasources/stackdriver.md | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index ef85de435c7..6968a0f6bd7 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -19,16 +19,16 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat 1. Open the side menu by clicking the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. 3. Click the `+ Add data source` button in the top header. -4. Select `Stackdriver` from the *Type* dropdown. +4. Select `Stackdriver` from the _Type_ dropdown. 5. Upload or paste in the Service Account Key file. See below for steps on how to create a Service Account Key file. > NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization. -Name | Description ------------- | ------------- -*Name* | The datasource name. This is how you refer to the datasource in panels & queries. -*Default* | Default datasource means that it will be pre-selected for new panels. -*Service Account Key* | Service Account Key File for a GCP Project. Instructions below on how to create it. +| Name | Description | +| --------------------- | ----------------------------------------------------------------------------------- | +| _Name_ | The datasource name. This is how you refer to the datasource in panels & queries. | +| _Default_ | Default datasource means that it will be pre-selected for new panels. | +| _Service Account Key_ | Service Account Key File for a GCP Project. Instructions below on how to create it. | ## Authentication @@ -40,8 +40,8 @@ To authenticate with the Stackdriver API, you need to create a Google Cloud Plat The following APIs need to be enabled first: -- [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) -- [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com) +* [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) +* [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com) Click on the links above and click the `Enable` button: @@ -52,18 +52,21 @@ Click on the links above and click the `Enable` button: 1. Navigate to the [APIs & Services Credentials page](https://console.cloud.google.com/apis/credentials). 2. Click on the `Create credentials` dropdown/button and choose the `Service account key` option. - ![Create service account button](/img/docs/v54/stackdriver_create_service_account_button.png) + ![Create service account button](/img/docs/v54/stackdriver_create_service_account_button.png) + 3. On the `Create service account key` page, choose key type `JSON`. Then in the `Service Account` dropdown, choose the `New service account` option: - ![Create service account key](/img/docs/v54/stackdriver_create_service_account_key.png) + ![Create service account key](/img/docs/v54/stackdriver_create_service_account_key.png) + 4. Some new fields will appear. Fill in a name for the service account in the `Service account name` field and then choose the `Monitoring Viewer` role from the `Role` dropdown: - ![Choose role](/img/docs/v54/stackdriver_service_account_choose_role.png) + ![Choose role](/img/docs/v54/stackdriver_service_account_choose_role.png) + 5. Click the Create button. A JSON key file will be created and downloaded to your computer. Store this file in a secure place as it allows access to your Stackdriver data. 6. Upload it to Grafana on the datasource Configuration page. You can either upload the file or paste in the contents of the file. - ![Choose role](/img/docs/v54/stackdriver_grafana_upload_key.png) + ![Choose role](/img/docs/v54/stackdriver_grafana_upload_key.png) 7. The file contents will be encrypted and saved in the Grafana database. Don't forget to save after uploading the file! - ![Choose role](/img/docs/v54/stackdriver_grafana_key_uploaded.png) + ![Choose role](/img/docs/v54/stackdriver_grafana_key_uploaded.png) ## Metric Query Editor @@ -82,9 +85,9 @@ The `Aligner` field allows you to align multiple time series after the same grou The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Stackdriver default groupings (which allows you to compare graphs in Grafana with graphs in the Stackdriver UI). The option is called `Stackdriver auto` and the defaults are: -- 1m for time ranges < 5 hours -- 5m for time ranges > 5 hours and < 23 hours -- 1h for time ranges > 23 hours +* 1m for time ranges < 23 hours +* 5m for time ranges >= 23 hours and < 6 days +* 1h for time ranges >= 6 days The other automatic option is `Grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the graph panel. Read more about the details [here](http://docs.grafana.org/reference/templating/#the-interval-variable). @@ -100,20 +103,20 @@ The Alias By field allows you to control the format of the legend keys. The defa #### Metric Type Patterns -Alias Pattern | Description | Example Result ------------------ | ---------------------------- | ------------- -`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` -`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` -`{{metric.service}}` | returns the service part | `compute` +| Alias Pattern | Description | Example Result | +| -------------------- | ---------------------------- | ------------------------------------------------- | +| `{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` | +| `{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` | +| `{{metric.service}}` | returns the service part | `compute` | #### Label Patterns In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns. -Alias Pattern Format | Description | Alias Pattern Example | Example Result ----------------------- | ---------------------------------- | ---------------------------- | ------------- -`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` -`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` +| Alias Pattern Format | Description | Alias Pattern Example | Example Result | +| ------------------------ | -------------------------------- | -------------------------------- | ---------------- | +| `{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` | +| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` | Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}` @@ -136,10 +139,10 @@ Writing variable queries is not supported yet. There are two syntaxes: -- `$` Example: rate(http_requests_total{job=~"$job"}[5m]) -- `[[varname]]` Example: rate(http_requests_total{job=~"[[job]]"}[5m]) +* `$` Example: rate(http_requests_total{job=~"$job"}[5m]) +* `[[varname]]` Example: rate(http_requests_total{job=~"[[job]]"}[5m]) -Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the *Multi-value* or *Include all value* options are enabled, Grafana converts the labels from plain text to a regex compatible string, which means you have to use `=~` instead of `=`. +Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_ options are enabled, Grafana converts the labels from plain text to a regex compatible string, which means you have to use `=~` instead of `=`. ## Annotations From c099074d2cd5f00d9df06173aad3ca0b282e9084 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 10:41:51 +0200 Subject: [PATCH 212/878] stackdriver: remove debug logging --- pkg/tsdb/stackdriver/stackdriver.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 77cbfcfbf9d..b854e590ab0 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -18,7 +18,6 @@ import ( "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -193,7 +192,6 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { if alignmentPeriod == "stackdriver-auto" { alignmentPeriodValue := int(math.Max(float64(durationSeconds), 60.0)) - logger.Info("alignmentPeriodValue", "alignmentPeriodValue", alignmentPeriodValue) if alignmentPeriodValue < 60*60*23 { alignmentPeriod = "+60s" } else if alignmentPeriodValue < 60*60*24*6 { From b724ca5b93b3df2dfd15a3e1845989d91ae1c2e5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 10:42:28 +0200 Subject: [PATCH 213/878] stackdriver: pass interval from panel to backend --- public/app/plugins/datasource/stackdriver/datasource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 5b6b5fd2a04..15f003f4c74 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -27,6 +27,7 @@ export default class StackdriverDatasource { } return { refId: t.refId, + intervalMs: options.intervalMs, datasourceId: this.id, metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}), primaryAggregation: this.templateSrv.replace(t.aggregation.crossSeriesReducer, options.scopedVars || {}), From e2bda4d321e26e4dddbbf764dd077a3c8e766077 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 10:55:11 +0200 Subject: [PATCH 214/878] stackdriver: fix typescript errors --- .../app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts | 1 - .../app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 30dd4fa5c54..1afb464bc12 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,7 +1,6 @@ import angular from 'angular'; import _ from 'lodash'; import * as options from './constants'; -import * as options from './constants'; import kbn from 'app/core/utils/kbn'; export class StackdriverAggregation { diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts index e22af17d605..68fbcbdb2a8 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts @@ -418,6 +418,7 @@ function createTarget(existingFilters?: string[]) { name: '', }, metricType: 'ametric', + service: '', refId: 'A', aggregation: { crossSeriesReducer: '', From 3fab616239aef644e416a75a8db0f67beb3d32be Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 27 Sep 2018 11:14:44 +0200 Subject: [PATCH 215/878] implement sql queries for transactional alert reminders --- pkg/models/alert_notifications.go | 26 +++--- pkg/services/sqlstore/alert_notification.go | 61 +++++++++++--- .../sqlstore/alert_notification_test.go | 80 +++++++++++-------- pkg/services/sqlstore/migrations/alert_mig.go | 5 +- 4 files changed, 118 insertions(+), 54 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 419f5048af4..54220e7d120 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -8,8 +8,17 @@ import ( ) var ( - ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") - ErrJournalingNotFound = errors.New("alert notification journaling not found") + ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") + ErrAlertNotificationStateNotFound = errors.New("alert notification state not found") + ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict") + ErrAlertNotificationStateAllreadyExist = errors.New("alert notification state allready exists.") +) + +type AlertNotificationStateType string + +var ( + AlertNotificationStatePending = AlertNotificationStateType("pending") + AlertNotificationStateCompleted = AlertNotificationStateType("completed") ) type AlertNotification struct { @@ -82,16 +91,15 @@ type AlertNotificationState struct { AlertId int64 NotifierId int64 SentAt int64 - State string + State AlertNotificationStateType Version int64 } type UpdateAlertNotificationStateCommand struct { - OrgId int64 - AlertId int64 - NotifierId int64 - SentAt int64 - State bool + Id int64 + SentAt int64 + State AlertNotificationStateType + Version int64 } type GetNotificationStateQuery struct { @@ -107,5 +115,5 @@ type InsertAlertNotificationCommand struct { AlertId int64 NotifierId int64 SentAt int64 - State string + State AlertNotificationStateType } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index ece06614d4a..4a115bcd788 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -18,8 +18,8 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) - bus.AddHandlerCtx("sql", RecordNotificationJournal) - bus.AddHandlerCtx("sql", GetLatestNotification) + bus.AddHandlerCtx("sql", InsertAlertNotificationState) + bus.AddHandlerCtx("sql", GetAlertNotificationState) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -228,34 +228,73 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { }) } -func RecordNotificationJournal(ctx context.Context, cmd *m.UpdateAlertNotificationStateCommand) error { +func InsertAlertNotificationState(ctx context.Context, cmd *m.InsertAlertNotificationCommand) error { return withDbSession(ctx, func(sess *DBSession) error { - journalEntry := &m.AlertNotificationState{ + notificationState := &m.AlertNotificationState{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, NotifierId: cmd.NotifierId, SentAt: cmd.SentAt, + State: cmd.State, + } + + _, err := sess.Insert(notificationState) + + if err == nil { + return nil + } + + if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + return m.ErrAlertNotificationStateAllreadyExist } - _, err := sess.Insert(journalEntry) return err }) } -func GetLatestNotification(ctx context.Context, cmd *m.GetNotificationStateQuery) error { +func UpdateAlertNotificationState(ctx context.Context, cmd *m.UpdateAlertNotificationStateCommand) error { + return withDbSession(ctx, func(sess *DBSession) error { + sql := `UPDATE alert_notification_state SET + state= ?, + version = ? + WHERE + id = ? AND + version = ? + ` + + res, err := sess.Exec(sql, cmd.State, cmd.Version+1, cmd.Id, cmd.Version) + if err != nil { + return err + } + + affected, _ := res.RowsAffected() + + if affected == 0 { + return m.ErrAlertNotificationStateVersionConflict + } + + return nil + }) +} + +func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQuery) error { return withDbSession(ctx, func(sess *DBSession) error { nj := &m.AlertNotificationState{} - err := sess.Desc("alert_notification_journal.sent_at"). - Where("alert_notification_journal.org_id = ?", cmd.OrgId). - Where("alert_notification_journal.alert_id = ?", cmd.AlertId). - Where("alert_notification_journal.notifier_id = ?", cmd.NotifierId). - Find(&nj) + exist, err := sess.Desc("alert_notification_state.sent_at"). + Where("alert_notification_state.org_id = ?", cmd.OrgId). + Where("alert_notification_state.alert_id = ?", cmd.AlertId). + Where("alert_notification_state.notifier_id = ?", cmd.NotifierId). + Get(nj) if err != nil { return err } + if !exist { + return m.ErrAlertNotificationStateNotFound + } + cmd.Result = nj return nil }) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 742d459e1ca..4b71b0d09dd 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -14,37 +14,53 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - Convey("Alert notification journal", func() { + Convey("Alert notification state", func() { var alertId int64 = 7 var orgId int64 = 5 var notifierId int64 = 10 - Convey("Getting last journal should raise error if no one exists", func() { - query := &m.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - err := GetLatestNotification(context.Background(), query) - So(err, ShouldNotBeNil) + Convey("Getting no existant state returns error", func() { + query := &models.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetAlertNotificationState(context.Background(), query) + So(err, ShouldEqual, models.ErrAlertNotificationStateNotFound) + }) - // recording an journal entry in another org to make sure org filter works as expected. - journalInOtherOrg := &m.UpdateAlertNotificationStateCommand{AlertId: alertId, NotifierId: notifierId, OrgId: 10, SentAt: 1} - err = RecordNotificationJournal(context.Background(), journalInOtherOrg) + Convey("Can insert new state for alert notifier", func() { + createCmd := &models.InsertAlertNotificationCommand{ + AlertId: alertId, + NotifierId: notifierId, + OrgId: orgId, + SentAt: 1, + State: models.AlertNotificationStateCompleted, + } + + err := InsertAlertNotificationState(context.Background(), createCmd) So(err, ShouldBeNil) - Convey("should be able to record two journaling events", func() { - createCmd := &m.UpdateAlertNotificationStateCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, SentAt: 1} + err = InsertAlertNotificationState(context.Background(), createCmd) + So(err, ShouldEqual, models.ErrAlertNotificationStateAllreadyExist) - err := RecordNotificationJournal(context.Background(), createCmd) + Convey("should be able to update alert notifier state", func() { + updateCmd := &models.UpdateAlertNotificationStateCommand{ + Id: 1, + SentAt: 1, + State: models.AlertNotificationStatePending, + Version: 0, + } + + err := UpdateAlertNotificationState(context.Background(), updateCmd) So(err, ShouldBeNil) - createCmd.SentAt += 1000 //increase epoch - - err = RecordNotificationJournal(context.Background(), createCmd) - So(err, ShouldBeNil) + Convey("should not be able to update older versions", func() { + err = UpdateAlertNotificationState(context.Background(), updateCmd) + So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) + }) }) }) }) Convey("Alert notifications should be empty", func() { - cmd := &m.GetAlertNotificationsQuery{ + cmd := &models.GetAlertNotificationsQuery{ OrgId: 2, Name: "email", } @@ -55,7 +71,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Cannot save alert notifier with send reminder = true", func() { - cmd := &m.CreateAlertNotificationCommand{ + cmd := &models.CreateAlertNotificationCommand{ Name: "ops", Type: "email", OrgId: 1, @@ -65,7 +81,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("and missing frequency", func() { err := CreateAlertNotificationCommand(cmd) - So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound) }) Convey("invalid frequency", func() { @@ -77,7 +93,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Cannot update alert notifier with send reminder = false", func() { - cmd := &m.CreateAlertNotificationCommand{ + cmd := &models.CreateAlertNotificationCommand{ Name: "ops update", Type: "email", OrgId: 1, @@ -88,14 +104,14 @@ func TestAlertNotificationSQLAccess(t *testing.T) { err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) - updateCmd := &m.UpdateAlertNotificationCommand{ + updateCmd := &models.UpdateAlertNotificationCommand{ Id: cmd.Result.Id, SendReminder: true, } Convey("and missing frequency", func() { err := UpdateAlertNotification(updateCmd) - So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound) }) Convey("invalid frequency", func() { @@ -108,7 +124,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can save Alert Notification", func() { - cmd := &m.CreateAlertNotificationCommand{ + cmd := &models.CreateAlertNotificationCommand{ Name: "ops", Type: "email", OrgId: 1, @@ -130,7 +146,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can update alert notification", func() { - newCmd := &m.UpdateAlertNotificationCommand{ + newCmd := &models.UpdateAlertNotificationCommand{ Name: "NewName", Type: "webhook", OrgId: cmd.Result.OrgId, @@ -146,7 +162,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can update alert notification to disable sending of reminders", func() { - newCmd := &m.UpdateAlertNotificationCommand{ + newCmd := &models.UpdateAlertNotificationCommand{ Name: "NewName", Type: "webhook", OrgId: cmd.Result.OrgId, @@ -161,12 +177,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) @@ -175,7 +191,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) Convey("search", func() { - query := &m.GetAlertNotificationsToSendQuery{ + query := &models.GetAlertNotificationsToSendQuery{ Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231}, OrgId: 1, } @@ -186,7 +202,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("all", func() { - query := &m.GetAllAlertNotificationsQuery{ + query := &models.GetAllAlertNotificationsQuery{ OrgId: 1, } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index d6886f6ecae..877dafcf1e1 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -122,10 +122,11 @@ func addAlertMigrations(mg *Migrator) { {Name: "version", Type: DB_BigInt, Nullable: false}, }, Indices: []*Index{ - {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: UniqueIndex}, }, } mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state)) - mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, notification_journal.Indices[0])) + mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", + NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) } From c5278af6c498020d206eebe601f7dcd23160668b Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 27 Sep 2018 11:33:13 +0200 Subject: [PATCH 216/878] add support for mysql and postgres unique index error codes --- pkg/models/alert_notifications.go | 10 +++-- pkg/services/alerting/notifier.go | 2 + pkg/services/sqlstore/alert_notification.go | 43 ++++++++++++++++--- .../sqlstore/alert_notification_test.go | 17 +++++--- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 54220e7d120..c3c41dc5dd9 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -11,7 +11,7 @@ var ( ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") ErrAlertNotificationStateNotFound = errors.New("alert notification state not found") ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict") - ErrAlertNotificationStateAllreadyExist = errors.New("alert notification state allready exists.") + ErrAlertNotificationStateAlreadyExist = errors.New("alert notification state already exists.") ) type AlertNotificationStateType string @@ -95,13 +95,17 @@ type AlertNotificationState struct { Version int64 } -type UpdateAlertNotificationStateCommand struct { +type SetAlertNotificationStateToPendingCommand struct { Id int64 SentAt int64 - State AlertNotificationStateType Version int64 } +type SetAlertNotificationStateToCompleteCommand struct { + Id int64 + SentAt int64 +} + type GetNotificationStateQuery struct { OrgId int64 AlertId int64 diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index ec448a8c1b0..941d3df9dc3 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -64,6 +64,8 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + // insert if needed + // Verify that we can send the notification again // but this time within the same transaction. // if !evalContext.IsTestRun && !not.ShouldNotify(ctx, evalContext) { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 4a115bcd788..92860c361aa 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -20,6 +20,8 @@ func init() { bus.AddHandler("sql", GetAllAlertNotifications) bus.AddHandlerCtx("sql", InsertAlertNotificationState) bus.AddHandlerCtx("sql", GetAlertNotificationState) + bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand) + bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -244,25 +246,54 @@ func InsertAlertNotificationState(ctx context.Context, cmd *m.InsertAlertNotific return nil } - if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { - return m.ErrAlertNotificationStateAllreadyExist + uniqenessIndexFailureCodes := []string{ + "UNIQUE constraint failed", + "pq: duplicate key value violates unique constraint", + "Error 1062: Duplicate entry ", + } + + for _, code := range uniqenessIndexFailureCodes { + if strings.HasPrefix(err.Error(), code) { + return m.ErrAlertNotificationStateAlreadyExist + } } return err }) } -func UpdateAlertNotificationState(ctx context.Context, cmd *m.UpdateAlertNotificationStateCommand) error { +func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { + return withDbSession(ctx, func(sess *DBSession) error { + sql := `UPDATE alert_notification_state SET + state= ? + WHERE + id = ?` + + res, err := sess.Exec(sql, m.AlertNotificationStateCompleted, cmd.Id) + if err != nil { + return err + } + + affected, _ := res.RowsAffected() + + if affected == 0 { + return m.ErrAlertNotificationStateVersionConflict + } + + return nil + }) +} + +func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToPendingCommand) error { return withDbSession(ctx, func(sess *DBSession) error { sql := `UPDATE alert_notification_state SET state= ?, version = ? WHERE id = ? AND - version = ? - ` + version = ?` - res, err := sess.Exec(sql, cmd.State, cmd.Version+1, cmd.Id, cmd.Version) + res, err := sess.Exec(sql, m.AlertNotificationStatePending, cmd.Version+1, cmd.Id, cmd.Version) if err != nil { return err } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 4b71b0d09dd..206c96b5c6a 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -38,23 +38,28 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) err = InsertAlertNotificationState(context.Background(), createCmd) - So(err, ShouldEqual, models.ErrAlertNotificationStateAllreadyExist) + So(err, ShouldEqual, models.ErrAlertNotificationStateAlreadyExist) Convey("should be able to update alert notifier state", func() { - updateCmd := &models.UpdateAlertNotificationStateCommand{ + updateCmd := &models.SetAlertNotificationStateToPendingCommand{ Id: 1, SentAt: 1, - State: models.AlertNotificationStatePending, Version: 0, } - err := UpdateAlertNotificationState(context.Background(), updateCmd) + err := SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) So(err, ShouldBeNil) - Convey("should not be able to update older versions", func() { - err = UpdateAlertNotificationState(context.Background(), updateCmd) + Convey("should not be able to set pending on old version", func() { + err = SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) }) + + Convey("should be able to set state to completed", func() { + cmd := &models.SetAlertNotificationStateToCompleteCommand{Id: 1} + err = SetAlertNotificationStateToCompleteCommand(context.Background(), cmd) + So(err, ShouldBeNil) + }) }) }) }) From 353a836128b588e365ddc04b1f17b278f7921814 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 25 Sep 2018 16:23:43 +0200 Subject: [PATCH 217/878] wip: Reactify the api keys page #13411 --- public/app/features/api-keys/ApiKeysPage.tsx | 94 +++++++++++++++++++ public/app/features/api-keys/state/actions.ts | 37 ++++++++ .../app/features/api-keys/state/reducers.ts | 16 ++++ public/app/routes/routes.ts | 8 ++ public/app/store/configureStore.ts | 2 + public/app/types/apiKeys.ts | 11 +++ public/app/types/index.ts | 3 + 7 files changed, 171 insertions(+) create mode 100644 public/app/features/api-keys/ApiKeysPage.tsx create mode 100644 public/app/features/api-keys/state/actions.ts create mode 100644 public/app/features/api-keys/state/reducers.ts create mode 100644 public/app/types/apiKeys.ts diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx new file mode 100644 index 00000000000..e0b4da28c40 --- /dev/null +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -0,0 +1,94 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { hot } from 'react-hot-loader'; +import { NavModel, ApiKey } from '../../types'; +import { getNavModel } from 'app/core/selectors/navModel'; +// import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import { loadApiKeys, deleteApiKey } from './state/actions'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; + +export interface Props { + navModel: NavModel; + apiKeys: ApiKey[]; + searchQuery: string; + loadApiKeys: typeof loadApiKeys; + deleteApiKey: typeof deleteApiKey; + // loadTeams: typeof loadTeams; + // deleteTeam: typeof deleteTeam; + // setSearchQuery: typeof setSearchQuery; +} + +export class ApiKeysPage extends PureComponent { + componentDidMount() { + this.fetchApiKeys(); + } + + async fetchApiKeys() { + await this.props.loadApiKeys(); + } + + deleteApiKey(id: number) { + return () => { + this.props.deleteApiKey(id); + }; + } + + render() { + const { navModel, apiKeys } = this.props; + + return ( +
    + +
    +

    Existing Keys

    +
    + + + + + + + {apiKeys.length > 0 ? ( + + {apiKeys.map(key => { + // id, name, role + return ( + + + + + + ); + })} + + ) : null} +
    NameRole +
    {key.name}{key.role} + + + +
    +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'apikeys'), + apiKeys: state.apiKeys.keys, + // searchQuery: getSearchQuery(state.teams), + }; +} + +const mapDispatchToProps = { + loadApiKeys, + deleteApiKey, + // loadTeams, + // deleteTeam, + // setSearchQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage)); diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts new file mode 100644 index 00000000000..494b562b3c9 --- /dev/null +++ b/public/app/features/api-keys/state/actions.ts @@ -0,0 +1,37 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState, ApiKey } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; + +export enum ActionTypes { + LoadApiKeys = 'LOAD_API_KEYS', +} + +export interface LoadApiKeysAction { + type: ActionTypes.LoadApiKeys; + payload: ApiKey[]; +} + +export type Action = LoadApiKeysAction; + +type ThunkResult = ThunkAction; + +const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({ + type: ActionTypes.LoadApiKeys, + payload: apiKeys, +}); + +export function loadApiKeys(): ThunkResult { + return async dispatch => { + const response = await getBackendSrv().get('/api/auth/keys'); + dispatch(apiKeysLoaded(response)); + }; +} + +export function deleteApiKey(id: number): ThunkResult { + return async dispatch => { + getBackendSrv() + .delete('/api/auth/keys/' + id) + .then(dispatch(loadApiKeys())); + }; +} diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts new file mode 100644 index 00000000000..6d45ccbfa03 --- /dev/null +++ b/public/app/features/api-keys/state/reducers.ts @@ -0,0 +1,16 @@ +import { ApiKeysState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialApiKeysState: ApiKeysState = { keys: [] }; + +export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => { + switch (action.type) { + case ActionTypes.LoadApiKeys: + return { ...state, keys: action.payload }; + } + return state; +}; + +export default { + apiKeys: apiKeysReducer, +}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 015b4ae0b51..9b90e374769 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,6 +5,7 @@ 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 ApiKeys from 'app/features/api-keys/ApiKeysPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; @@ -141,6 +142,13 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { templateUrl: 'public/app/features/org/partials/orgApiKeys.html', controller: 'OrgApiKeysCtrl', }) + .when('/org/apikeys2', { + template: '', + resolve: { + roles: () => ['Editor', 'Admin'], + component: () => ApiKeys, + }, + }) .when('/org/teams', { template: '', resolve: { diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 8f6cf25043d..3988dad0cd8 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -4,6 +4,7 @@ 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 apiKeysReducers from 'app/features/api-keys/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; @@ -11,6 +12,7 @@ const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...apiKeysReducers, ...foldersReducers, ...dashboardReducers, }); diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts new file mode 100644 index 00000000000..56d3e930504 --- /dev/null +++ b/public/app/types/apiKeys.ts @@ -0,0 +1,11 @@ +import { OrgRole } from './acl'; + +export interface ApiKey { + id: number; + name: string; + role: OrgRole; +} + +export interface ApiKeysState { + keys: ApiKey[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 778a1b21b55..8c50ea88782 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -7,6 +7,7 @@ import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; import { PluginMeta } from './plugins'; +import { ApiKey, ApiKeysState } from './apiKeys'; export { Team, @@ -33,6 +34,8 @@ export { PermissionLevel, DataSource, PluginMeta, + ApiKey, + ApiKeysState, }; export interface StoreState { From e8ba35ab2d5d56f13a2eb6cdcb0bad14518cb7dd Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 10:31:43 +0200 Subject: [PATCH 218/878] Move User type out of UserPicker and into app/types --- public/app/core/components/Picker/UserPicker.tsx | 8 +------- public/app/types/index.ts | 2 ++ public/app/types/user.ts | 6 ++++++ 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 public/app/types/user.ts diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index e50513c44e1..8f48ba8f66a 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -3,6 +3,7 @@ import Select from 'react-select'; import PickerOption from './PickerOption'; import { debounce } from 'lodash'; import { getBackendSrv } from 'app/core/services/backend_srv'; +import { User } from 'app/types'; export interface Props { onSelected: (user: User) => void; @@ -14,13 +15,6 @@ export interface State { isLoading: boolean; } -export interface User { - id: number; - label: string; - avatarUrl: string; - login: string; -} - export class UserPicker extends Component { debouncedSearch: any; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8c50ea88782..bd219282f52 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -8,6 +8,7 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; import { PluginMeta } from './plugins'; import { ApiKey, ApiKeysState } from './apiKeys'; +import { User } from './user'; export { Team, @@ -36,6 +37,7 @@ export { PluginMeta, ApiKey, ApiKeysState, + User, }; export interface StoreState { diff --git a/public/app/types/user.ts b/public/app/types/user.ts new file mode 100644 index 00000000000..9c13e6b027b --- /dev/null +++ b/public/app/types/user.ts @@ -0,0 +1,6 @@ +export interface User { + id: number; + label: string; + avatarUrl: string; + login: string; +} From 97d718f87a07854982d04622eff541a35c5b72eb Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 13:39:16 +0200 Subject: [PATCH 219/878] Pick up the type from app/types --- public/app/core/components/PermissionList/AddPermission.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 77ac6953b74..fc062ce63e4 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -1,7 +1,8 @@ import React, { Component } from 'react'; -import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { UserPicker } 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 { User } from 'app/types'; import { dashboardPermissionLevels, dashboardAclTargets, From cc0802cc39f2049e2994309e2601bb6fcc46200b Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 13:40:56 +0200 Subject: [PATCH 220/878] Pick up the type from app/types --- public/app/features/teams/TeamMembers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index cda175f4395..588745eea37 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,10 +1,10 @@ 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 { UserPicker } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { TeamMember } from '../../types'; +import { TeamMember, User } from 'app/types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; From e3d579e410fe6a1f93bad62ca9edf52300e73d47 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 13:45:04 +0200 Subject: [PATCH 221/878] Add "search box" and a "add new" box to the new API Keys page #13411 --- public/app/features/api-keys/ApiKeysPage.tsx | 147 ++++++++++++++++-- public/app/features/api-keys/state/actions.ts | 23 ++- .../app/features/api-keys/state/reducers.ts | 7 +- .../app/features/api-keys/state/selectors.ts | 9 ++ public/app/types/apiKeys.ts | 6 + public/app/types/index.ts | 3 +- 6 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 public/app/features/api-keys/state/selectors.ts diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index e0b4da28c40..5ad292c7ba3 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -1,12 +1,13 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { NavModel, ApiKey } from '../../types'; +import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; +import { getApiKeys } from './state/selectors'; +import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; // import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { loadApiKeys, deleteApiKey } from './state/actions'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import SlideDown from 'app/core/components/Animations/SlideDown'; export interface Props { navModel: NavModel; @@ -14,12 +15,31 @@ export interface Props { searchQuery: string; loadApiKeys: typeof loadApiKeys; deleteApiKey: typeof deleteApiKey; - // loadTeams: typeof loadTeams; - // deleteTeam: typeof deleteTeam; - // setSearchQuery: typeof setSearchQuery; + setSearchQuery: typeof setSearchQuery; + addApiKey: typeof addApiKey; } +export interface State { + isAdding: boolean; + newApiKey: NewApiKey; +} + +enum ApiKeyStateProps { + Name = 'name', + Role = 'role', +} + +const initialApiKeyState = { + name: '', + role: OrgRole.Viewer, +}; + export class ApiKeysPage extends PureComponent { + constructor(props) { + super(props); + this.state = { isAdding: false, newApiKey: initialApiKeyState }; + } + componentDidMount() { this.fetchApiKeys(); } @@ -28,19 +48,120 @@ export class ApiKeysPage extends PureComponent { await this.props.loadApiKeys(); } - deleteApiKey(id: number) { + onDeleteApiKey(id: number) { return () => { this.props.deleteApiKey(id); }; } + onSearchQueryChange = evt => { + this.props.setSearchQuery(evt.target.value); + }; + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onAddApiKey = async evt => { + evt.preventDefault(); + this.props.addApiKey(this.state.newApiKey); + this.setState((prevState: State) => { + return { + ...prevState, + newApiKey: initialApiKeyState, + }; + }); + }; + + onApiKeyStateUpdate = (evt, prop: string) => { + const value = evt.currentTarget.value; + this.setState((prevState: State) => { + const newApiKey = { + ...prevState.newApiKey, + }; + newApiKey[prop] = value; + + return { + ...prevState, + newApiKey: newApiKey, + }; + }); + }; + render() { - const { navModel, apiKeys } = this.props; + const { newApiKey, isAdding } = this.state; + const { navModel, apiKeys, searchQuery } = this.props; return (
    +
    +
    + +
    + +
    + + {/* +
    + + +
    + +
    Add API Key
    +
    +
    +
    + Key name + this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} + /> +
    +
    + Role + + + +
    +
    + +
    +
    +
    +
    +
    +

    Existing Keys

    @@ -59,7 +180,7 @@ export class ApiKeysPage extends PureComponent { @@ -78,7 +199,8 @@ export class ApiKeysPage extends PureComponent { function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'apikeys'), - apiKeys: state.apiKeys.keys, + apiKeys: getApiKeys(state.apiKeys), + searchQuery: state.apiKeys.searchQuery, // searchQuery: getSearchQuery(state.teams), }; } @@ -86,9 +208,8 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadApiKeys, deleteApiKey, - // loadTeams, - // deleteTeam, - // setSearchQuery, + setSearchQuery, + addApiKey, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage)); diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts index 494b562b3c9..934852e1b19 100644 --- a/public/app/features/api-keys/state/actions.ts +++ b/public/app/features/api-keys/state/actions.ts @@ -1,10 +1,10 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState, ApiKey } from 'app/types'; -import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; export enum ActionTypes { LoadApiKeys = 'LOAD_API_KEYS', + SetApiKeysSearchQuery = 'SET_API_KEYS_SEARCH_QUERY', } export interface LoadApiKeysAction { @@ -12,15 +12,27 @@ export interface LoadApiKeysAction { payload: ApiKey[]; } -export type Action = LoadApiKeysAction; +export interface SetSearchQueryAction { + type: ActionTypes.SetApiKeysSearchQuery; + payload: string; +} -type ThunkResult = ThunkAction; +export type Action = LoadApiKeysAction | SetSearchQueryAction; + +type ThunkResult = ThunkAction; const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({ type: ActionTypes.LoadApiKeys, payload: apiKeys, }); +export function addApiKey(apiKey: ApiKey): ThunkResult { + return async dispatch => { + await getBackendSrv().post('/api/auth/keys', apiKey); + dispatch(loadApiKeys()); + }; +} + export function loadApiKeys(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/auth/keys'); @@ -35,3 +47,8 @@ export function deleteApiKey(id: number): ThunkResult { .then(dispatch(loadApiKeys())); }; } + +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetApiKeysSearchQuery, + payload: searchQuery, +}); diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts index 6d45ccbfa03..a21aa55dbf7 100644 --- a/public/app/features/api-keys/state/reducers.ts +++ b/public/app/features/api-keys/state/reducers.ts @@ -1,12 +1,17 @@ import { ApiKeysState } from 'app/types'; import { Action, ActionTypes } from './actions'; -export const initialApiKeysState: ApiKeysState = { keys: [] }; +export const initialApiKeysState: ApiKeysState = { + keys: [], + searchQuery: '', +}; export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => { switch (action.type) { case ActionTypes.LoadApiKeys: return { ...state, keys: action.payload }; + case ActionTypes.SetApiKeysSearchQuery: + return { ...state, searchQuery: action.payload }; } return state; }; diff --git a/public/app/features/api-keys/state/selectors.ts b/public/app/features/api-keys/state/selectors.ts new file mode 100644 index 00000000000..8065c252e85 --- /dev/null +++ b/public/app/features/api-keys/state/selectors.ts @@ -0,0 +1,9 @@ +import { ApiKeysState } from 'app/types'; + +export const getApiKeys = (state: ApiKeysState) => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.keys.filter(key => { + return regex.test(key.name) || regex.test(key.role); + }); +}; diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index 56d3e930504..6288f5165ad 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -6,6 +6,12 @@ export interface ApiKey { role: OrgRole; } +export interface NewApiKey { + name: string; + role: OrgRole; +} + export interface ApiKeysState { keys: ApiKey[]; + searchQuery: string; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index bd219282f52..42460ecb9c6 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -7,7 +7,7 @@ import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; import { PluginMeta } from './plugins'; -import { ApiKey, ApiKeysState } from './apiKeys'; +import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { User } from './user'; export { @@ -37,6 +37,7 @@ export { PluginMeta, ApiKey, ApiKeysState, + NewApiKey, User, }; From 60866d16b1bcf9674e3e22b3fe98d854a2de5267 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 14:58:27 +0200 Subject: [PATCH 222/878] Add tests for ApiKeysPage #13411 --- .../features/api-keys/ApiKeysPage.test.tsx | 73 +++ public/app/features/api-keys/ApiKeysPage.tsx | 10 +- .../api-keys/__mocks__/apiKeysMock.ts | 22 + .../__snapshots__/ApiKeysPage.test.tsx.snap | 430 ++++++++++++++++++ .../app/features/teams/__mocks__/teamMocks.ts | 2 +- 5 files changed, 529 insertions(+), 8 deletions(-) create mode 100644 public/app/features/api-keys/ApiKeysPage.test.tsx create mode 100644 public/app/features/api-keys/__mocks__/apiKeysMock.ts create mode 100644 public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx new file mode 100644 index 00000000000..518180fc424 --- /dev/null +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, ApiKeysPage } from './ApiKeysPage'; +import { NavModel, ApiKey } from 'app/types'; +import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + apiKeys: [] as ApiKey[], + searchQuery: '', + loadApiKeys: jest.fn(), + deleteApiKey: jest.fn(), + setSearchQuery: jest.fn(), + addApiKey: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as ApiKeysPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render API keys table', () => { + const { wrapper } = setup({ + apiKeys: getMultipleMockKeys(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadApiKeys', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadApiKeys).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.onDeleteApiKey(getMockKey()); + expect(instance.props.deleteApiKey).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/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 5ad292c7ba3..25077c59a62 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -48,10 +48,8 @@ export class ApiKeysPage extends PureComponent { await this.props.loadApiKeys(); } - onDeleteApiKey(id: number) { - return () => { - this.props.deleteApiKey(id); - }; + onDeleteApiKey(key: ApiKey) { + this.props.deleteApiKey(key.id); } onSearchQueryChange = evt => { @@ -111,8 +109,6 @@ export class ApiKeysPage extends PureComponent {
    - - {/* @@ -180,7 +176,7 @@ export class ApiKeysPage extends PureComponent {
    diff --git a/public/app/features/api-keys/__mocks__/apiKeysMock.ts b/public/app/features/api-keys/__mocks__/apiKeysMock.ts new file mode 100644 index 00000000000..117f0d6d0c6 --- /dev/null +++ b/public/app/features/api-keys/__mocks__/apiKeysMock.ts @@ -0,0 +1,22 @@ +import { ApiKey, OrgRole } from 'app/types'; + +export const getMultipleMockKeys = (numberOfKeys: number): ApiKey[] => { + const keys: ApiKey[] = []; + for (let i = 1; i <= numberOfKeys; i++) { + keys.push({ + id: i, + name: `test-${i}`, + role: OrgRole.Viewer, + }); + } + + return keys; +}; + +export const getMockKey = (): ApiKey => { + return { + id: 1, + name: 'test', + role: OrgRole.Admin, + }; +}; diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap new file mode 100644 index 00000000000..92f27d701d9 --- /dev/null +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -0,0 +1,430 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render API keys table 1`] = ` +
    + +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add API Key +
    +
    +
    +
    + + Key name + + +
    +
    + + Role + + + + +
    +
    + +
    +
    + +
    +
    +

    + Existing Keys +

    +
    {key.name} {key.role} - + {key.name} {key.role} - +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Role + +
    + test-1 + + Viewer + + + + +
    + test-2 + + Viewer + + + + +
    + test-3 + + Viewer + + + + +
    + test-4 + + Viewer + + + + +
    + test-5 + + Viewer + + + + +
    +
    +
    +`; + +exports[`Render should render component 1`] = ` +
    + +
    +
    +
    + +
    +
    + +
    + +
    + +
    + Add API Key +
    +
    +
    +
    + + Key name + + +
    +
    + + Role + + + + +
    +
    + +
    +
    +
    +
    +
    +

    + Existing Keys +

    + + + + + + + +
    + Name + + Role + +
    +
    +
    +`; diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 34fa06b2d09..339f227c081 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team, TeamGroup, TeamMember } from '../../../types'; +import { Team, TeamGroup, TeamMember } from 'app/types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { const teams: Team[] = []; From 32fb24f248180c8e3d6b05783c59f00080ded9e8 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 26 Sep 2018 15:03:22 +0200 Subject: [PATCH 223/878] Update test-snapshot, remove dead code #13411 --- public/app/features/api-keys/ApiKeysPage.tsx | 3 +-- .../api-keys/__snapshots__/ApiKeysPage.test.tsx.snap | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 25077c59a62..b2aefcb1fa0 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -176,7 +176,7 @@ export class ApiKeysPage extends PureComponent { {key.name} {key.role} - + this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> @@ -197,7 +197,6 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'apikeys'), apiKeys: getApiKeys(state.apiKeys), searchQuery: state.apiKeys.searchQuery, - // searchQuery: getSearchQuery(state.teams), }; } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 92f27d701d9..77c7f620173 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -174,6 +174,7 @@ exports[`Render should render API keys table 1`] = ` Date: Thu, 27 Sep 2018 09:31:05 +0200 Subject: [PATCH 224/878] Add tests for the reducers & selectors for API keys #13411 --- public/app/features/api-keys/ApiKeysPage.tsx | 1 - .../features/api-keys/state/reducers.test.ts | 31 +++++++++++++++++++ .../features/api-keys/state/selectors.test.ts | 25 +++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 public/app/features/api-keys/state/reducers.test.ts create mode 100644 public/app/features/api-keys/state/selectors.test.ts diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index b2aefcb1fa0..3225fd7d2b5 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -170,7 +170,6 @@ export class ApiKeysPage extends PureComponent { {apiKeys.length > 0 ? ( {apiKeys.map(key => { - // id, name, role return ( {key.name} diff --git a/public/app/features/api-keys/state/reducers.test.ts b/public/app/features/api-keys/state/reducers.test.ts new file mode 100644 index 00000000000..3b2c831a5a3 --- /dev/null +++ b/public/app/features/api-keys/state/reducers.test.ts @@ -0,0 +1,31 @@ +import { Action, ActionTypes } from './actions'; +import { initialApiKeysState, apiKeysReducer } from './reducers'; +import { getMultipleMockKeys } from '../__mocks__/apiKeysMock'; + +describe('API Keys reducer', () => { + it('should set keys', () => { + const payload = getMultipleMockKeys(4); + + const action: Action = { + type: ActionTypes.LoadApiKeys, + payload, + }; + + const result = apiKeysReducer(initialApiKeysState, action); + + expect(result.keys).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test query'; + + const action: Action = { + type: ActionTypes.SetApiKeysSearchQuery, + payload, + }; + + const result = apiKeysReducer(initialApiKeysState, action); + + expect(result.searchQuery).toEqual('test query'); + }); +}); diff --git a/public/app/features/api-keys/state/selectors.test.ts b/public/app/features/api-keys/state/selectors.test.ts new file mode 100644 index 00000000000..7d8f3122ce6 --- /dev/null +++ b/public/app/features/api-keys/state/selectors.test.ts @@ -0,0 +1,25 @@ +import { getApiKeys } from './selectors'; +import { getMultipleMockKeys } from '../__mocks__/apiKeysMock'; +import { ApiKeysState } from 'app/types'; + +describe('API Keys selectors', () => { + describe('Get API Keys', () => { + const mockKeys = getMultipleMockKeys(5); + + it('should return all keys if no search query', () => { + const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '' }; + + const keys = getApiKeys(mockState); + + expect(keys).toEqual(mockKeys); + }); + + it('should filter keys if search query exists', () => { + const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5' }; + + const keys = getApiKeys(mockState); + + expect(keys.length).toEqual(1); + }); + }); +}); From c7fb6916b9292420a1589c595be1e184e0a0699b Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 27 Sep 2018 11:26:47 +0200 Subject: [PATCH 225/878] Open modal with API key information after key is added #13411 --- .../api-keys/ApiKeysAddedModal.test.tsx | 25 ++++++ .../features/api-keys/ApiKeysAddedModal.tsx | 46 +++++++++++ public/app/features/api-keys/ApiKeysPage.tsx | 17 +++- .../ApiKeysAddedModal.test.tsx.snap | 78 +++++++++++++++++++ public/app/features/api-keys/state/actions.ts | 6 +- 5 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 public/app/features/api-keys/ApiKeysAddedModal.test.tsx create mode 100644 public/app/features/api-keys/ApiKeysAddedModal.tsx create mode 100644 public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap diff --git a/public/app/features/api-keys/ApiKeysAddedModal.test.tsx b/public/app/features/api-keys/ApiKeysAddedModal.test.tsx new file mode 100644 index 00000000000..160418a7ab8 --- /dev/null +++ b/public/app/features/api-keys/ApiKeysAddedModal.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ApiKeysAddedModal, Props } from './ApiKeysAddedModal'; + +const setup = (propOverrides?: object) => { + const props: Props = { + apiKey: 'api key test', + rootPath: 'test/path', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + + return { + wrapper, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/api-keys/ApiKeysAddedModal.tsx b/public/app/features/api-keys/ApiKeysAddedModal.tsx new file mode 100644 index 00000000000..995aa46c773 --- /dev/null +++ b/public/app/features/api-keys/ApiKeysAddedModal.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +export interface Props { + apiKey: string; + rootPath: string; +} + +export const ApiKeysAddedModal = (props: Props) => { + return ( + + ); +}; + +export default ApiKeysAddedModal; diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 3225fd7d2b5..2f19250e835 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -1,13 +1,16 @@ import React, { PureComponent } from 'react'; +import ReactDOMServer from 'react-dom/server'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { getApiKeys } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; -// import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import ApiKeysAddedModal from './ApiKeysAddedModal'; +import config from 'app/core/config'; +import appEvents from 'app/core/app_events'; export interface Props { navModel: NavModel; @@ -62,7 +65,17 @@ export class ApiKeysPage extends PureComponent { onAddApiKey = async evt => { evt.preventDefault(); - this.props.addApiKey(this.state.newApiKey); + + const openModal = (apiKey: string) => { + const rootPath = window.location.origin + config.appSubUrl; + const modalTemplate = ReactDOMServer.renderToString(); + + appEvents.emit('show-modal', { + templateHtml: modalTemplate, + }); + }; + + this.props.addApiKey(this.state.newApiKey, openModal); this.setState((prevState: State) => { return { ...prevState, diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap new file mode 100644 index 00000000000..0fcb13308eb --- /dev/null +++ b/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    +

    + + + API Key Created + +

    + + + +
    +
    +
    +
    + + Key + + + api key test + +
    +
    +
    + You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now. +
    +
    + You can authenticate request using the Authorization HTTP header, example: +
    +
    +
    +        curl -H "Authorization: Bearer 
    +        api key test
    +        " 
    +        test/path
    +        /api/dashboards/home
    +      
    +
    +
    +
    +`; diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts index 934852e1b19..63e91088476 100644 --- a/public/app/features/api-keys/state/actions.ts +++ b/public/app/features/api-keys/state/actions.ts @@ -26,10 +26,12 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({ payload: apiKeys, }); -export function addApiKey(apiKey: ApiKey): ThunkResult { +export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult { return async dispatch => { - await getBackendSrv().post('/api/auth/keys', apiKey); + const result = await getBackendSrv().post('/api/auth/keys', apiKey); + dispatch(setSearchQuery('')); dispatch(loadApiKeys()); + openModal(result.key); }; } From 362010c43816c426e97534a50f1bcbda44f737f5 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 27 Sep 2018 11:34:28 +0200 Subject: [PATCH 226/878] Remove angular code related to API Keys and point the route to the React component #13411 --- public/app/features/org/all.ts | 1 - public/app/features/org/org_api_keys_ctrl.ts | 44 ----------------- .../features/org/partials/apikeyModal.html | 37 -------------- .../app/features/org/partials/orgApiKeys.html | 49 ------------------- public/app/routes/routes.ts | 4 -- 5 files changed, 135 deletions(-) delete mode 100644 public/app/features/org/org_api_keys_ctrl.ts delete mode 100644 public/app/features/org/partials/apikeyModal.html delete mode 100644 public/app/features/org/partials/orgApiKeys.html diff --git a/public/app/features/org/all.ts b/public/app/features/org/all.ts index 8872450e3ab..1cbca483138 100644 --- a/public/app/features/org/all.ts +++ b/public/app/features/org/all.ts @@ -6,6 +6,5 @@ import './change_password_ctrl'; import './new_org_ctrl'; import './user_invite_ctrl'; import './create_team_ctrl'; -import './org_api_keys_ctrl'; import './org_details_ctrl'; import './prefs_control'; diff --git a/public/app/features/org/org_api_keys_ctrl.ts b/public/app/features/org/org_api_keys_ctrl.ts deleted file mode 100644 index 1ead0a350b9..00000000000 --- a/public/app/features/org/org_api_keys_ctrl.ts +++ /dev/null @@ -1,44 +0,0 @@ -import angular from 'angular'; - -export class OrgApiKeysCtrl { - /** @ngInject */ - constructor($scope, $http, backendSrv, navModelSrv) { - $scope.navModel = navModelSrv.getNav('cfg', 'apikeys', 0); - - $scope.roleTypes = ['Viewer', 'Editor', 'Admin']; - $scope.token = { role: 'Viewer' }; - - $scope.init = () => { - $scope.getTokens(); - }; - - $scope.getTokens = () => { - backendSrv.get('/api/auth/keys').then(tokens => { - $scope.tokens = tokens; - }); - }; - - $scope.removeToken = id => { - backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens); - }; - - $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; - - $scope.appEvent('show-modal', { - src: 'public/app/features/org/partials/apikeyModal.html', - scope: modalScope, - }); - - $scope.getTokens(); - }); - }; - - $scope.init(); - } -} - -angular.module('grafana.controllers').controller('OrgApiKeysCtrl', OrgApiKeysCtrl); diff --git a/public/app/features/org/partials/apikeyModal.html b/public/app/features/org/partials/apikeyModal.html deleted file mode 100644 index eeefcafc634..00000000000 --- a/public/app/features/org/partials/apikeyModal.html +++ /dev/null @@ -1,37 +0,0 @@ - - diff --git a/public/app/features/org/partials/orgApiKeys.html b/public/app/features/org/partials/orgApiKeys.html deleted file mode 100644 index a2b4ceb6670..00000000000 --- a/public/app/features/org/partials/orgApiKeys.html +++ /dev/null @@ -1,49 +0,0 @@ - - -
    - -

    Add new

    - -
    -
    -
    - Key name - -
    -
    - Role - - - -
    -
    - -
    -
    -
    - -

    Existing Keys

    - - - - - - - - - - - - - - - -
    NameRole
    {{t.name}}{{t.role}} - - - -
    -
    - - - diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 9b90e374769..470153f5dd1 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -139,10 +139,6 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/org/apikeys', { - templateUrl: 'public/app/features/org/partials/orgApiKeys.html', - controller: 'OrgApiKeysCtrl', - }) - .when('/org/apikeys2', { template: '', resolve: { roles: () => ['Editor', 'Admin'], From 5c24fa68a5da8da124fd224dfdf2084ad6b528a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Sep 2018 11:57:28 +0200 Subject: [PATCH 227/878] explore: fixes to dark theme, fixes #13349 --- public/app/features/explore/Explore.tsx | 9 +++++---- public/sass/components/_form_select_box.scss | 18 ++++++------------ public/sass/pages/_explore.scss | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 187d68133cd..50d894de43f 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -528,10 +528,11 @@ export class Explore extends React.Component { {!datasourceMissing ? (
    + ng-change="refresh()">
    + + +
    +
    +
    + Key + {props.apiKey} +
    +
    + +
    + You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now. +
    +
    + You can authenticate request using the Authorization HTTP header, example: +
    +
    +
    +            curl -H "Authorization: Bearer {props.apiKey}" {props.rootPath}/api/dashboards/home
    +          
    +
    +
    +
    @@ -21,7 +21,7 @@
    + ng-change="refresh()">
    diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 1afb464bc12..30089588da6 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -24,32 +24,10 @@ export class StackdriverAggregationCtrl { this.setAggOptions(); this.setAlignOptions(); $scope.alignmentPeriods = options.alignmentPeriods; - $scope.onAlignmentChange = this.onAlignmentChange.bind(this); - $scope.onAggregationChange = this.onAggregationChange.bind(this); $scope.formatAlignmentText = this.formatAlignmentText.bind(this); $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); } - onAlignmentChange(newVal: string) { - if (newVal === 'ALIGN_NONE') { - this.$scope.target.aggregation.crossSeriesReducer = 'REDUCE_NONE'; - } - this.$scope.refresh(); - } - - onAggregationChange(newVal: string) { - if (newVal !== 'REDUCE_NONE' && this.$scope.target.aggregation.perSeriesAligner === 'ALIGN_NONE') { - const newAlignmentOption = options.alignOptions.find( - o => - o.value !== 'ALIGN_NONE' && - o.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && - o.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 - ); - this.$scope.target.aggregation.perSeriesAligner = newAlignmentOption ? newAlignmentOption.value : ''; - } - this.$scope.refresh(); - } - setAlignOptions() { this.$scope.alignOptions = !this.$scope.target.valueType ? [] @@ -60,8 +38,8 @@ export class StackdriverAggregationCtrl { ); }); if (!this.$scope.alignOptions.find(o => o.value === this.$scope.target.aggregation.perSeriesAligner)) { - const newValue = this.$scope.alignOptions.find(o => o.value !== 'ALIGN_NONE'); - this.$scope.target.aggregation.perSeriesAligner = newValue ? newValue.value : ''; + this.$scope.target.aggregation.perSeriesAligner = + this.$scope.alignOptions.length > 0 ? this.$scope.alignOptions[0].value : ''; } } diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index efc935dd338..d3a20deed77 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -29,34 +29,5 @@ describe('StackdriverAggregationCtrl', () => { }); }); }); - - describe('when a user selects ALIGN_NONE and a reducer is selected', () => { - beforeEach(async () => { - ctrl = new StackdriverAggregationCtrl({ - $on: () => {}, - refresh: () => {}, - target: { aggregation: { crossSeriesReducer: 'RANDOM_REDUCER' } }, - }); - ctrl.onAlignmentChange('ALIGN_NONE'); - }); - it('should set REDUCE_NONE as selected aggregation', () => { - expect(ctrl.$scope.target.aggregation.crossSeriesReducer).toBe('REDUCE_NONE'); - }); - }); - - describe('when a user a user select a reducer and no alignment is selected', () => { - beforeEach(async () => { - ctrl = new StackdriverAggregationCtrl({ - $on: () => {}, - refresh: () => {}, - target: { aggregation: { crossSeriesReducer: 'REDUCE_NONE', perSeriesAligner: 'ALIGN_NONE' } }, - }); - ctrl.onAggregationChange('ALIGN_NONE'); - }); - - it('should set an alignment', () => { - expect(ctrl.$scope.target.aggregation.perSeriesAligner).not.toBe('ALIGN_NONE'); - }); - }); }); }); From 8ae72bce073dfa528289141d165409a096cd482f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 14:45:15 +0200 Subject: [PATCH 237/878] stackdriver: fix typescript error --- .../app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts index 68fbcbdb2a8..bf2da913ba1 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts @@ -417,6 +417,7 @@ function createTarget(existingFilters?: string[]) { id: '', name: '', }, + unit: '', metricType: 'ametric', service: '', refId: 'A', From b899a0e1c188f4ffa4d652573e533c9e66a561b0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Thu, 27 Sep 2018 14:45:36 +0200 Subject: [PATCH 238/878] revert rename --- public/app/features/plugins/PluginList.tsx | 2 +- public/app/features/plugins/PluginListItem.tsx | 4 ++-- public/app/features/plugins/PluginListPage.test.tsx | 4 ++-- public/app/features/plugins/PluginListPage.tsx | 4 ++-- public/app/features/plugins/__mocks__/pluginMocks.ts | 4 ++-- public/app/features/plugins/state/actions.ts | 6 +++--- public/app/features/plugins/state/reducers.ts | 4 ++-- public/app/types/index.ts | 4 ++-- public/app/types/plugins.ts | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx index f080b67f78d..2c47e755de3 100644 --- a/public/app/features/plugins/PluginList.tsx +++ b/public/app/features/plugins/PluginList.tsx @@ -1,7 +1,7 @@ import React, { SFC } from 'react'; import classNames from 'classnames/bind'; import PluginListItem from './PluginListItem'; -import { PluginListItem } from 'app/types'; +import { Plugin } from 'app/types'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; interface Props { diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx index 2bfe401d5b3..05eac614fd5 100644 --- a/public/app/features/plugins/PluginListItem.tsx +++ b/public/app/features/plugins/PluginListItem.tsx @@ -1,8 +1,8 @@ import React, { SFC } from 'react'; -import { PluginListItem } from 'app/types'; +import { Plugin } from 'app/types'; interface Props { - plugin: PluginListItem; + plugin: Plugin; } const PluginListItem: SFC = props => { diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx index 866d63c9437..452a89837c7 100644 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { PluginListPage, Props } from './PluginListPage'; -import { NavModel, PluginListItem } from '../../types'; +import { NavModel, Plugin } from '../../types'; import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; const setup = (propOverrides?: object) => { const props: Props = { navModel: {} as NavModel, - plugins: [] as PluginListItem[], + plugins: [] as Plugin[], layoutMode: LayoutModes.Grid, loadPlugins: jest.fn(), }; diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 4a330ece7f4..de2968b126c 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import PageHeader from '../../core/components/PageHeader/PageHeader'; import PluginActionBar from './PluginActionBar'; import PluginList from './PluginList'; -import { NavModel, PluginListItem } from '../../types'; +import { NavModel, Plugin } from '../../types'; import { loadPlugins } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; import { getLayoutMode, getPlugins } from './state/selectors'; @@ -12,7 +12,7 @@ import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector' export interface Props { navModel: NavModel; - plugins: PluginListItem[]; + plugins: Plugin[]; layoutMode: LayoutMode; loadPlugins: typeof loadPlugins; } diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index d34997d0ffa..d8dd67d5b61 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -1,6 +1,6 @@ -import { PluginListItem } from 'app/types'; +import { Plugin } from 'app/types'; -export const getMockPlugins = (amount: number): PluginListItem[] => { +export const getMockPlugins = (amount: number): Plugin[] => { const plugins = []; for (let i = 0; i <= amount; i++) { diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts index 51c3e5241d0..24774c6061c 100644 --- a/public/app/features/plugins/state/actions.ts +++ b/public/app/features/plugins/state/actions.ts @@ -1,4 +1,4 @@ -import { PluginListItem, StoreState } from 'app/types'; +import { Plugin, StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; @@ -11,7 +11,7 @@ export enum ActionTypes { export interface LoadPluginsAction { type: ActionTypes.LoadPlugins; - payload: PluginListItem[]; + payload: Plugin[]; } export interface SetPluginsSearchQueryAction { @@ -34,7 +34,7 @@ export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryActio payload: query, }); -const pluginsLoaded = (plugins: PluginListItem[]): LoadPluginsAction => ({ +const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ type: ActionTypes.LoadPlugins, payload: plugins, }); diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts index f643a80ce54..1ca2880282c 100644 --- a/public/app/features/plugins/state/reducers.ts +++ b/public/app/features/plugins/state/reducers.ts @@ -1,9 +1,9 @@ import { Action, ActionTypes } from './actions'; -import { PluginListItem, PluginsState } from 'app/types'; +import { Plugin, PluginsState } from 'app/types'; import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector'; export const initialState: PluginsState = { - plugins: [] as PluginListItem[], + plugins: [] as Plugin[], searchQuery: '', layoutMode: LayoutModes.Grid, }; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 33bcffc1790..1dd11d73564 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -6,7 +6,7 @@ import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; -import { PluginMeta, PluginListItem, PluginsState } from './plugins'; +import { PluginMeta, Plugin, PluginsState } from './plugins'; export { Team, @@ -33,7 +33,7 @@ export { PermissionLevel, DataSource, PluginMeta, - PluginListItem, + Plugin, PluginsState, }; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 6dcb4cefe02..92bebfef8d4 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -27,7 +27,7 @@ export interface PluginMetaInfo { version: string; } -export interface PluginListItem { +export interface Plugin { defaultNavUrl: string; enabled: boolean; hasUpdate: boolean; @@ -41,7 +41,7 @@ export interface PluginListItem { } export interface PluginsState { - plugins: PluginListItem[]; + plugins: Plugin[]; searchQuery: string; layoutMode: string; } From 7d44aacf4afd704852a3ccc5cacde616354f609e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Sep 2018 14:50:14 +0200 Subject: [PATCH 239/878] refactoring: slight changes to PR #13247 --- public/app/features/dashboard/submenu/submenu.html | 4 ++-- .../{text_variable.ts => TextBoxVariable.ts} | 10 +++++----- public/app/features/templating/all.ts | 4 ++-- public/app/features/templating/partials/editor.html | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename public/app/features/templating/{text_variable.ts => TextBoxVariable.ts} (88%) diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 9d3332b06e2..d7cee33e6c3 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -4,8 +4,8 @@ - - + +
    diff --git a/public/app/features/templating/text_variable.ts b/public/app/features/templating/TextBoxVariable.ts similarity index 88% rename from public/app/features/templating/text_variable.ts rename to public/app/features/templating/TextBoxVariable.ts index 3459b99f602..331ff4f95b8 100644 --- a/public/app/features/templating/text_variable.ts +++ b/public/app/features/templating/TextBoxVariable.ts @@ -1,13 +1,13 @@ import { Variable, assignModelProperties, variableTypes } from './variable'; -export class TextVariable implements Variable { +export class TextBoxVariable implements Variable { query: string; current: any; options: any[]; skipUrlSync: boolean; defaults = { - type: 'text', + type: 'textbox', name: '', hide: 2, label: '', @@ -51,8 +51,8 @@ export class TextVariable implements Variable { } } -variableTypes['text'] = { - name: 'Text', - ctor: TextVariable, +variableTypes['textbox'] = { + name: 'Text box', + ctor: TextBoxVariable, description: 'Define a textbox variable, where users can enter any arbitrary string', }; diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 424494f66b2..b872fa6cd4a 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -9,7 +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'; +import { TextBoxVariable } from './TextBoxVariable'; coreModule.factory('templateSrv', () => { return templateSrv; @@ -23,5 +23,5 @@ export { CustomVariable, ConstantVariable, AdhocVariable, - TextVariable + TextBoxVariable, }; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index ed8398738da..ac4450c20a2 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -155,10 +155,10 @@
    -
    +
    Text options
    - Value + Default value
    From 11ee65d35a6c9fbab094bb950598ae3a3a779b87 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Thu, 27 Sep 2018 14:51:00 +0200 Subject: [PATCH 240/878] deletez --- public/app/features/plugins/PluginList.tsx | 2 +- public/app/features/plugins/all.ts | 1 - .../plugins/partials/plugin_list.html | 45 ------------------- .../app/features/plugins/plugin_list_ctrl.ts | 30 ------------- 4 files changed, 1 insertion(+), 77 deletions(-) delete mode 100644 public/app/features/plugins/partials/plugin_list.html delete mode 100644 public/app/features/plugins/plugin_list_ctrl.ts diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx index 2c47e755de3..0074839e754 100644 --- a/public/app/features/plugins/PluginList.tsx +++ b/public/app/features/plugins/PluginList.tsx @@ -5,7 +5,7 @@ import { Plugin } from 'app/types'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; interface Props { - plugins: PluginListItem[]; + plugins: Plugin[]; layoutMode: LayoutMode; } diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index fd19ea963b6..5be7593f68d 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,6 +1,5 @@ import './plugin_edit_ctrl'; import './plugin_page_ctrl'; -import './plugin_list_ctrl'; import './import_list/import_list'; import './ds_edit_ctrl'; import './ds_dashboards_ctrl'; diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html deleted file mode 100644 index 04b5bf9c791..00000000000 --- a/public/app/features/plugins/partials/plugin_list.html +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts deleted file mode 100644 index 315252364cc..00000000000 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ /dev/null @@ -1,30 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; - -export class PluginListCtrl { - plugins: any[]; - tabIndex: number; - navModel: any; - searchQuery: string; - allPlugins: any[]; - - /** @ngInject */ - constructor(private backendSrv: any, $location, navModelSrv) { - this.tabIndex = 0; - this.navModel = navModelSrv.getNav('cfg', 'plugins', 0); - - this.backendSrv.get('api/plugins', { embedded: 0 }).then(plugins => { - this.plugins = plugins; - this.allPlugins = plugins; - }); - } - - onQueryUpdated() { - const regex = new RegExp(this.searchQuery, 'ig'); - this.plugins = _.filter(this.allPlugins, item => { - return regex.test(item.name) || regex.test(item.type); - }); - } -} - -angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl); From 5873a7132471297331d1bca7d42663a951e748df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Sep 2018 14:58:46 +0200 Subject: [PATCH 241/878] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ecc3339600..e0fd5caf819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212) * **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon) * **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030) +* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173) # 5.3.0 (unreleased) From 81bdf86bf8153a430f75e4bd41567d5b582b5ff5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 15:05:26 +0200 Subject: [PATCH 242/878] stackdriver: es6 style directive, avoid using scope --- .../partials/query.aggregation.html | 16 +++---- .../stackdriver/query_aggregation_ctrl.ts | 42 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html index 3bf57606367..379b9a36dc3 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html @@ -2,25 +2,25 @@
    -
    -
    +
    -
    @@ -33,14 +33,14 @@
    -
    \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 30089588da6..fc6a708a45f 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -19,51 +19,55 @@ export class StackdriverAggregation { } export class StackdriverAggregationCtrl { + alignmentPeriods: any[]; + alignmentPeriod: string; + aggOptions: any[]; + alignOptions: any[]; + target: any; + constructor(private $scope) { - $scope.aggOptions = options.aggOptions; + this.$scope.ctrl = this; + this.target = $scope.target; + this.alignmentPeriod = $scope.alignmentPeriod; + this.alignmentPeriods = options.alignmentPeriods; + this.aggOptions = options.aggOptions; + this.alignOptions = options.alignOptions; this.setAggOptions(); this.setAlignOptions(); - $scope.alignmentPeriods = options.alignmentPeriods; - $scope.formatAlignmentText = this.formatAlignmentText.bind(this); $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); } setAlignOptions() { - this.$scope.alignOptions = !this.$scope.target.valueType + this.alignOptions = !this.target.valueType ? [] : options.alignOptions.filter(i => { return ( - i.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && - i.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 + i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 ); }); - if (!this.$scope.alignOptions.find(o => o.value === this.$scope.target.aggregation.perSeriesAligner)) { - this.$scope.target.aggregation.perSeriesAligner = - this.$scope.alignOptions.length > 0 ? this.$scope.alignOptions[0].value : ''; + if (!this.alignOptions.find(o => o.value === this.target.aggregation.perSeriesAligner)) { + this.target.aggregation.perSeriesAligner = this.alignOptions.length > 0 ? this.alignOptions[0].value : ''; } } setAggOptions() { - this.$scope.aggOptions = !this.$scope.target.metricKind + this.aggOptions = !this.target.metricKind ? [] : options.aggOptions.filter(i => { return ( - i.valueTypes.indexOf(this.$scope.target.valueType) !== -1 && - i.metricKinds.indexOf(this.$scope.target.metricKind) !== -1 + i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 ); }); - if (!this.$scope.aggOptions.find(o => o.value === this.$scope.target.aggregation.crossSeriesReducer)) { - const newValue = this.$scope.aggOptions.find(o => o.value !== 'REDUCE_NONE'); - this.$scope.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + if (!this.aggOptions.find(o => o.value === this.target.aggregation.crossSeriesReducer)) { + const newValue = this.aggOptions.find(o => o.value !== 'REDUCE_NONE'); + this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; } } formatAlignmentText() { - const selectedAlignment = this.$scope.alignOptions.find( - ap => ap.value === this.$scope.target.aggregation.perSeriesAligner - ); - return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`; + const selectedAlignment = this.alignOptions.find(ap => ap.value === this.target.aggregation.perSeriesAligner); + return `${kbn.secondsToHms(this.alignmentPeriod)} interval (${selectedAlignment.text})`; } } From a3b0539754481559934f33919b15bcd222556698 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 15:05:47 +0200 Subject: [PATCH 243/878] stackdriver: update tests --- .../stackdriver/specs/query_aggregation_ctrl.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index d3a20deed77..3887381c9a8 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -14,16 +14,16 @@ describe('StackdriverAggregationCtrl', () => { it('should populate all aggregate options except two', () => { ctrl.setAggOptions(); - expect(ctrl.$scope.aggOptions.length).toBe(11); - expect(ctrl.$scope.aggOptions.map(o => o.value)).toEqual( + expect(ctrl.aggOptions.length).toBe(11); + expect(ctrl.aggOptions.map(o => o.value)).toEqual( expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); it('should populate all alignment options except two', () => { ctrl.setAlignOptions(); - expect(ctrl.$scope.alignOptions.length).toBe(10); - expect(ctrl.$scope.alignOptions.map(o => o.value)).toEqual( + expect(ctrl.alignOptions.length).toBe(9); + expect(ctrl.alignOptions.map(o => o.value)).toEqual( expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) ); }); From 88f36cbd22b1b0d9631fcd7bf34de10f7fa87b88 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 27 Sep 2018 15:15:41 +0200 Subject: [PATCH 244/878] Compile TS of the whole project to detect type errors - was not covered by TS lint - TS errors are only noticed in broken builds - added grunt task to run `tsc --noEmit` --- scripts/grunt/default_task.js | 9 +++++---- scripts/grunt/options/exec.js | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index 07519cdd6c8..bfa4080da4c 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -1,5 +1,5 @@ // Lint and build CSS -module.exports = function(grunt) { +module.exports = function (grunt) { 'use strict'; grunt.registerTask('default', [ @@ -18,15 +18,16 @@ module.exports = function(grunt) { grunt.registerTask('precommit', [ 'sasslint', 'exec:tslint', + 'exec:tsc', 'no-only-tests' ]); - grunt.registerTask('no-only-tests', function() { + grunt.registerTask('no-only-tests', function () { var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js'); - files.forEach(function(spec) { + files.forEach(function (spec) { var rows = grunt.file.read(spec).split('\n'); - rows.forEach(function(row) { + rows.forEach(function (row) { if (row.indexOf('.only(') > 0) { grunt.log.errorlns(row); grunt.fail.warn('found only statement in test: ' + spec) diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index 92e530cd5fd..d01a993be67 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -1,8 +1,9 @@ -module.exports = function(config, grunt) { +module.exports = function (config, grunt) { 'use strict'; return { tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', + tsc: 'yarn tsc --noEmit', jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2', webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', }; From 03b43ab7699c9fce5d900957a63b8427f5256202 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 27 Sep 2018 15:17:35 +0200 Subject: [PATCH 245/878] stackdriver: wip annotation support --- pkg/tsdb/stackdriver/annotation_query.go | 80 ++++++++++++++++++- pkg/tsdb/stackdriver/annotation_query_test.go | 36 ++++----- pkg/tsdb/stackdriver/stackdriver.go | 24 +++--- .../stackdriver/annotations_query_ctrl.ts | 32 ++++++++ .../plugins/datasource/stackdriver/module.ts | 7 +- .../partials/annotations.editor.html | 19 +++-- .../stackdriver/partials/query.editor.html | 3 +- .../stackdriver/partials/query.filter.html | 7 +- .../datasource/stackdriver/plugin.json | 2 +- .../stackdriver/query_filter_ctrl.ts | 22 +++-- 10 files changed, 165 insertions(+), 67 deletions(-) create mode 100644 public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts diff --git a/pkg/tsdb/stackdriver/annotation_query.go b/pkg/tsdb/stackdriver/annotation_query.go index 37e68a6d3bd..b8758eeb378 100644 --- a/pkg/tsdb/stackdriver/annotation_query.go +++ b/pkg/tsdb/stackdriver/annotation_query.go @@ -2,6 +2,7 @@ package stackdriver import ( "context" + "time" "github.com/grafana/grafana/pkg/tsdb" ) @@ -11,14 +12,85 @@ func (e *StackdriverExecutor) executeAnnotationQuery(ctx context.Context, tsdbQu Results: make(map[string]*tsdb.QueryResult), } - _, err := e.buildAnnotationQuery(tsdbQuery) + firstQuery := tsdbQuery.Queries[0] + + queries, err := e.buildQueries(tsdbQuery) if err != nil { return nil, err } - return result, nil + queryRes, resp, err := e.executeQuery(ctx, queries[0], tsdbQuery) + if err != nil { + return nil, err + } + title := firstQuery.Model.Get("title").MustString() + text := firstQuery.Model.Get("text").MustString() + tags := firstQuery.Model.Get("tags").MustString() + err = e.parseToAnnotations(queryRes, resp, queries[0], title, text, tags) + result.Results[firstQuery.RefId] = queryRes + + return result, err } -func (e *StackdriverExecutor) buildAnnotationQuery(tsdbQuery *tsdb.TsdbQuery) (*StackdriverQuery, error) { - return &StackdriverQuery{}, nil +func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery, title string, text string, tags string) error { + annotations := make([]map[string]string, 0) + + for _, series := range data.TimeSeries { + // reverse the order to be ascending + for i := len(series.Points) - 1; i >= 0; i-- { + point := series.Points[i] + + annotation := make(map[string]string) + annotation["time"] = point.Interval.EndTime.UTC().Format(time.RFC3339) + annotation["title"] = title + annotation["tags"] = tags + annotation["text"] = text + annotations = append(annotations, annotation) + } + } + + transformAnnotationToTable(annotations, queryRes) + return nil } + +func transformAnnotationToTable(data []map[string]string, result *tsdb.QueryResult) { + table := &tsdb.Table{ + Columns: make([]tsdb.TableColumn, 4), + Rows: make([]tsdb.RowValues, 0), + } + table.Columns[0].Text = "time" + table.Columns[1].Text = "title" + table.Columns[2].Text = "tags" + table.Columns[3].Text = "text" + + for _, r := range data { + values := make([]interface{}, 4) + values[0] = r["time"] + values[1] = r["title"] + values[2] = r["tags"] + values[3] = r["text"] + table.Rows = append(table.Rows, values) + } + result.Tables = append(result.Tables, table) + result.Meta.Set("rowCount", len(data)) + slog.Info("anno", "len", len(data)) +} + +// func (e *StackdriverExecutor) buildAnnotationQuery(tsdbQuery *tsdb.TsdbQuery) (*StackdriverQuery, error) { +// firstQuery := queryContext.Queries[0] + +// metricType := query.Model.Get("metricType").MustString() +// filterParts := query.Model.Get("filters").MustArray() +// filterString := buildFilterString(metricType, filterParts) +// params := url.Values{} +// params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339)) +// params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339)) +// params.Add("filter", buildFilterString(metricType, filterParts)) +// params.Add("view", "FULL") + +// return &StackdriverQuery{ +// RefID: firstQuery.RefID, +// Params: params, +// Target: "", +// }, nil +// } diff --git a/pkg/tsdb/stackdriver/annotation_query_test.go b/pkg/tsdb/stackdriver/annotation_query_test.go index 0fb47fbe453..16c0897fc05 100644 --- a/pkg/tsdb/stackdriver/annotation_query_test.go +++ b/pkg/tsdb/stackdriver/annotation_query_test.go @@ -1,9 +1,7 @@ package stackdriver import ( - "fmt" "testing" - "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/tsdb" @@ -14,28 +12,22 @@ import ( func TestStackdriverAnnotationQuery(t *testing.T) { Convey("Stackdriver Annotation Query Executor", t, func() { executor := &StackdriverExecutor{} - Convey("Parse queries from frontend and build Stackdriver API queries", func() { - fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) - tsdbQuery := &tsdb.TsdbQuery{ - TimeRange: &tsdb.TimeRange{ - From: fmt.Sprintf("%v", fromStart.Unix()*1000), - To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), - }, - Queries: []*tsdb.Query{ - { - Model: simplejson.NewFromAny(map[string]interface{}{ - "metricType": "a/metric/type", - "view": "FULL", - "type": "annotationQuery", - }), - RefId: "annotationQuery", - }, - }, - } - query, err := executor.buildAnnotationQuery(tsdbQuery) + Convey("When parsing the stackdriver api response", func() { + data, err := loadTestFile("./test-data/2-series-response-no-agg.json") + So(err, ShouldBeNil) + So(len(data.TimeSeries), ShouldEqual, 3) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "annotationQuery"} + query := &StackdriverQuery{} + err = executor.parseToAnnotations(res, data, query, "atitle", "atext", "atag") So(err, ShouldBeNil) - So(query, ShouldNotBeNil) + Convey("Should return annotations table", func() { + So(len(res.Tables), ShouldEqual, 1) + So(len(res.Tables[0].Rows), ShouldEqual, 9) + So(res.Tables[0].Rows[0][1], ShouldEqual, "atitle") + So(res.Tables[0].Rows[0][3], ShouldEqual, "atext") + }) }) }) } diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index b854e590ab0..847fb271c01 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -93,10 +93,14 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu } for _, query := range queries { - queryRes, err := e.executeQuery(ctx, query, tsdbQuery) + queryRes, resp, err := e.executeQuery(ctx, query, tsdbQuery) if err != nil { return nil, err } + err = e.parseResponse(queryRes, resp, query) + if err != nil { + queryRes.Error = err + } result.Results[query.RefID] = queryRes } @@ -219,13 +223,13 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { } } -func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *StackdriverQuery, tsdbQuery *tsdb.TsdbQuery) (*tsdb.QueryResult, error) { +func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *StackdriverQuery, tsdbQuery *tsdb.TsdbQuery) (*tsdb.QueryResult, StackdriverResponse, error) { queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID} req, err := e.createRequest(ctx, e.dsInfo) if err != nil { queryResult.Error = err - return queryResult, nil + return queryResult, StackdriverResponse{}, nil } req.URL.RawQuery = query.Params.Encode() @@ -257,22 +261,16 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv res, err := ctxhttp.Do(ctx, e.httpClient, req) if err != nil { queryResult.Error = err - return queryResult, nil + return queryResult, StackdriverResponse{}, nil } data, err := e.unmarshalResponse(res) if err != nil { queryResult.Error = err - return queryResult, nil + return queryResult, StackdriverResponse{}, nil } - err = e.parseResponse(queryResult, data, query) - if err != nil { - queryResult.Error = err - return queryResult, nil - } - - return queryResult, nil + return queryResult, data, nil } func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (StackdriverResponse, error) { @@ -429,7 +427,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. req, err := http.NewRequest(http.MethodGet, "https://monitoring.googleapis.com/", nil) if err != nil { - slog.Info("Failed to create request", "error", err) + slog.Error("Failed to create request", "error", err) return nil, fmt.Errorf("Failed to create request. error: %v", err) } diff --git a/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts b/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts new file mode 100644 index 00000000000..407707ed779 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts @@ -0,0 +1,32 @@ +import _ from 'lodash'; + +import './query_filter_ctrl'; + +export class StackdriverAnnotationsQueryCtrl { + static templateUrl = 'partials/annotations.editor.html'; + annotation: any; + datasource: any; + + defaultDropdownValue = 'Select Metric'; + defaultServiceValue = 'All Services'; + + defaults = { + project: { + id: 'default', + name: 'loading project...', + }, + metricType: this.defaultDropdownValue, + metricService: this.defaultServiceValue, + metric: '', + filters: [], + metricKind: '', + valueType: '', + }; + + /** @ngInject */ + constructor() { + this.annotation.target = this.annotation.target || {}; + this.annotation.target.refId = 'annotationQuery'; + _.defaultsDeep(this.annotation.target, this.defaults); + } +} diff --git a/public/app/plugins/datasource/stackdriver/module.ts b/public/app/plugins/datasource/stackdriver/module.ts index eba3bcfb950..183c5c9ff88 100644 --- a/public/app/plugins/datasource/stackdriver/module.ts +++ b/public/app/plugins/datasource/stackdriver/module.ts @@ -1,14 +1,11 @@ import StackdriverDatasource from './datasource'; import { StackdriverQueryCtrl } from './query_ctrl'; import { StackdriverConfigCtrl } from './config_ctrl'; - -// class AnnotationsQueryCtrl { -// static templateUrl = 'partials/annotations.editor.html'; -// } +import { StackdriverAnnotationsQueryCtrl } from './annotations_query_ctrl'; export { StackdriverDatasource as Datasource, StackdriverQueryCtrl as QueryCtrl, StackdriverConfigCtrl as ConfigCtrl, - // AnnotationsQueryCtrl, + StackdriverAnnotationsQueryCtrl as AnnotationsQueryCtrl, }; diff --git a/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html index 9d228b8e4f9..592dffacd51 100644 --- a/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html @@ -1,13 +1,16 @@ -
    + + +
    - Graphite query - + Title +
    - -
    Or
    -
    - Graphite events tags - + Text + +
    +
    +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 64fabeae38e..256a854830d 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -1,5 +1,6 @@ - +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.filter.html b/public/app/plugins/datasource/stackdriver/partials/query.filter.html index e962a4b8772..9ec59005a0b 100644 --- a/public/app/plugins/datasource/stackdriver/partials/query.filter.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.filter.html @@ -4,11 +4,6 @@
    -
    -
    -
    -
    -
    Metric
    -
    +
    Group By
    diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index 85672aacf2a..06a6880002b 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -4,7 +4,7 @@ "id": "stackdriver", "metrics": true, "alerting": true, - "annotations": false, + "annotations": true, "state": "beta", "queryOptions": { "maxDataPoints": true, diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index d843de1074a..b017f722b2c 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -16,6 +16,7 @@ export class StackdriverFilter { refresh: '&', defaultDropdownValue: '<', defaultServiceValue: '<', + hideGroupBys: '<', }, }; } @@ -54,15 +55,18 @@ export class StackdriverFilterCtrl { .then(this.loadMetricDescriptors.bind(this)) .then(this.getLabels.bind(this)); - this.initSegments(); + this.initSegments($scope.hideGroupBys); } - initSegments() { - this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => { - return this.uiSegmentSrv.getSegmentForValue(groupBy); - }); + initSegments(hideGroupBys: boolean) { + if (!hideGroupBys) { + this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => { + return this.uiSegmentSrv.getSegmentForValue(groupBy); + }); + this.ensurePlusButton(this.groupBySegments); + } + this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' }); - this.ensurePlusButton(this.groupBySegments); this.filterSegments = new FilterSegments( this.uiSegmentSrv, @@ -142,7 +146,11 @@ export class StackdriverFilterCtrl { this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; resolve(); } catch (error) { - console.log(error.data.message); + if (error.data && error.data.message) { + console.log(error.data.message); + } else { + console.log(error); + } appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.target.metricType]); resolve(); } From a028df658a1425d5035fca72a58e532276266916 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 15:42:01 +0200 Subject: [PATCH 246/878] stackdriver: set first metric as selected if no metric could be retrieved from the target --- .../plugins/datasource/stackdriver/annotations_query_ctrl.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts b/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts index 407707ed779..bccac02bf38 100644 --- a/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts @@ -1,5 +1,4 @@ import _ from 'lodash'; - import './query_filter_ctrl'; export class StackdriverAnnotationsQueryCtrl { @@ -16,7 +15,7 @@ export class StackdriverAnnotationsQueryCtrl { name: 'loading project...', }, metricType: this.defaultDropdownValue, - metricService: this.defaultServiceValue, + service: this.defaultServiceValue, metric: '', filters: [], metricKind: '', From 9351b56e576a6f4a888c465a9b279dcfd2aeaf85 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Sep 2018 15:59:17 +0200 Subject: [PATCH 247/878] stackdriver: fix alignment period bug --- .../plugins/datasource/stackdriver/query_aggregation_ctrl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index fc6a708a45f..50a4f630e50 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -20,7 +20,6 @@ export class StackdriverAggregation { export class StackdriverAggregationCtrl { alignmentPeriods: any[]; - alignmentPeriod: string; aggOptions: any[]; alignOptions: any[]; target: any; @@ -28,7 +27,6 @@ export class StackdriverAggregationCtrl { constructor(private $scope) { this.$scope.ctrl = this; this.target = $scope.target; - this.alignmentPeriod = $scope.alignmentPeriod; this.alignmentPeriods = options.alignmentPeriods; this.aggOptions = options.aggOptions; this.alignOptions = options.alignOptions; @@ -67,7 +65,7 @@ export class StackdriverAggregationCtrl { formatAlignmentText() { const selectedAlignment = this.alignOptions.find(ap => ap.value === this.target.aggregation.perSeriesAligner); - return `${kbn.secondsToHms(this.alignmentPeriod)} interval (${selectedAlignment.text})`; + return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`; } } From a63877bd4fc851a15a2757bb309d6ca3470ee7ff Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 27 Sep 2018 16:16:09 +0200 Subject: [PATCH 248/878] stackdriver: pattern formatting for annotations --- pkg/tsdb/stackdriver/annotation_query.go | 57 +++++++++++++------ pkg/tsdb/stackdriver/annotation_query_test.go | 6 +- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/pkg/tsdb/stackdriver/annotation_query.go b/pkg/tsdb/stackdriver/annotation_query.go index b8758eeb378..6345d7982ca 100644 --- a/pkg/tsdb/stackdriver/annotation_query.go +++ b/pkg/tsdb/stackdriver/annotation_query.go @@ -2,6 +2,8 @@ package stackdriver import ( "context" + "fmt" + "strings" "time" "github.com/grafana/grafana/pkg/tsdb" @@ -42,9 +44,9 @@ func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, dat annotation := make(map[string]string) annotation["time"] = point.Interval.EndTime.UTC().Format(time.RFC3339) - annotation["title"] = title + annotation["title"] = formatAnnotationText(title, point.Value.DoubleValue, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) annotation["tags"] = tags - annotation["text"] = text + annotation["text"] = formatAnnotationText(text, point.Value.DoubleValue, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) annotations = append(annotations, annotation) } } @@ -76,21 +78,40 @@ func transformAnnotationToTable(data []map[string]string, result *tsdb.QueryResu slog.Info("anno", "len", len(data)) } -// func (e *StackdriverExecutor) buildAnnotationQuery(tsdbQuery *tsdb.TsdbQuery) (*StackdriverQuery, error) { -// firstQuery := queryContext.Queries[0] +func formatAnnotationText(annotationText string, pointValue float64, metricType string, metricLabels map[string]string, resourceLabels map[string]string) string { + result := legendKeyFormat.ReplaceAllFunc([]byte(annotationText), func(in []byte) []byte { + metaPartName := strings.Replace(string(in), "{{", "", 1) + metaPartName = strings.Replace(metaPartName, "}}", "", 1) + metaPartName = strings.TrimSpace(metaPartName) -// metricType := query.Model.Get("metricType").MustString() -// filterParts := query.Model.Get("filters").MustArray() -// filterString := buildFilterString(metricType, filterParts) -// params := url.Values{} -// params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339)) -// params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339)) -// params.Add("filter", buildFilterString(metricType, filterParts)) -// params.Add("view", "FULL") + if metaPartName == "metric.type" { + return []byte(metricType) + } -// return &StackdriverQuery{ -// RefID: firstQuery.RefID, -// Params: params, -// Target: "", -// }, nil -// } + metricPart := replaceWithMetricPart(metaPartName, metricType) + + if metricPart != nil { + return metricPart + } + + if metaPartName == "value" { + return []byte(fmt.Sprintf("%f", pointValue)) + } + + metaPartName = strings.Replace(metaPartName, "metric.label.", "", 1) + + if val, exists := metricLabels[metaPartName]; exists { + return []byte(val) + } + + metaPartName = strings.Replace(metaPartName, "resource.label.", "", 1) + + if val, exists := resourceLabels[metaPartName]; exists { + return []byte(val) + } + + return in + }) + + return string(result) +} diff --git a/pkg/tsdb/stackdriver/annotation_query_test.go b/pkg/tsdb/stackdriver/annotation_query_test.go index 16c0897fc05..fd7545c3759 100644 --- a/pkg/tsdb/stackdriver/annotation_query_test.go +++ b/pkg/tsdb/stackdriver/annotation_query_test.go @@ -19,14 +19,14 @@ func TestStackdriverAnnotationQuery(t *testing.T) { res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "annotationQuery"} query := &StackdriverQuery{} - err = executor.parseToAnnotations(res, data, query, "atitle", "atext", "atag") + err = executor.parseToAnnotations(res, data, query, "atitle {{metric.label.instance_name}} {{value}}", "atext {{resource.label.zone}}", "atag") So(err, ShouldBeNil) Convey("Should return annotations table", func() { So(len(res.Tables), ShouldEqual, 1) So(len(res.Tables[0].Rows), ShouldEqual, 9) - So(res.Tables[0].Rows[0][1], ShouldEqual, "atitle") - So(res.Tables[0].Rows[0][3], ShouldEqual, "atext") + So(res.Tables[0].Rows[0][1], ShouldEqual, "atitle collector-asia-east-1 9.856650") + So(res.Tables[0].Rows[0][3], ShouldEqual, "atext asia-east1-a") }) }) }) From 15ce4746396f4ba17068e6a3def8909685c6d4d8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 27 Sep 2018 14:32:54 +0200 Subject: [PATCH 249/878] wip --- pkg/models/alert_notifications.go | 4 +- pkg/services/alerting/interfaces.go | 17 ++-- pkg/services/alerting/notifier.go | 96 ++++++++++++--------- pkg/services/alerting/notifiers/base.go | 11 +-- pkg/services/sqlstore/alert_notification.go | 32 ++++++- 5 files changed, 98 insertions(+), 62 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index c3c41dc5dd9..b46a09ea345 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -96,9 +96,7 @@ type AlertNotificationState struct { } type SetAlertNotificationStateToPendingCommand struct { - Id int64 - SentAt int64 - Version int64 + State *AlertNotificationState } type SetAlertNotificationStateToCompleteCommand struct { diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 46f8b3c769c..65369ed3884 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -3,6 +3,8 @@ package alerting import ( "context" "time" + + "github.com/grafana/grafana/pkg/models" ) type EvalHandler interface { @@ -20,7 +22,7 @@ type Notifier interface { NeedsImage() bool // ShouldNotify checks this evaluation should send an alert notification - ShouldNotify(ctx context.Context, evalContext *EvalContext) bool + ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool GetNotifierId() int64 GetIsDefault() bool @@ -28,11 +30,16 @@ type Notifier interface { GetFrequency() time.Duration } -type NotifierSlice []Notifier +type NotifierState struct { + notifier Notifier + state *models.AlertNotificationState +} -func (notifiers NotifierSlice) ShouldUploadImage() bool { - for _, notifier := range notifiers { - if notifier.NeedsImage() { +type NotifierStateSlice []*NotifierState + +func (notifiers NotifierStateSlice) ShouldUploadImage() bool { + for _, ns := range notifiers { + if ns.notifier.NeedsImage() { return true } } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 941d3df9dc3..d9d0e278e99 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,7 +1,6 @@ package alerting import ( - "context" "errors" "fmt" @@ -39,64 +38,59 @@ type notificationService struct { } func (n *notificationService) SendIfNeeded(context *EvalContext) error { - notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) + notifierStates, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) if err != nil { return err } - if len(notifiers) == 0 { + if len(notifierStates) == 0 { return nil } - if notifiers.ShouldUploadImage() { + if notifierStates.ShouldUploadImage() { if err = n.uploadImage(context); err != nil { n.log.Error("Failed to upload alert panel image.", "error", err) } } - return n.sendNotifications(context, notifiers) + // get alert notification (version = 1) + // phantomjs 15 sek + // loopa notifier - ge mig ett lås! where version = 1 + // send notification + // Släpp lås + // + + return n.sendNotifications(context, notifierStates) } -func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { - for _, notifier := range notifiers { - not := notifier +func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *NotifierState) error { + return nil +} - err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { - n.log.Debug("trying to send notification", "id", not.GetNotifierId()) +func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *NotifierState) error { + n.log.Debug("trying to send notification", "id", notifierState.notifier.GetNotifierId()) - // insert if needed + setPendingCmd := &m.SetAlertNotificationStateToPendingCommand{ + State: notifierState.state, + } - // Verify that we can send the notification again - // but this time within the same transaction. - // if !evalContext.IsTestRun && !not.ShouldNotify(ctx, evalContext) { - // return nil - // } + err := bus.DispatchCtx(evalContext.Ctx, setPendingCmd) + if err == m.ErrAlertNotificationStateVersionConflict { + return nil + } - // n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - // metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + if err != nil { + return err + } - // //send notification - // // success := not.Notify(evalContext) == nil - - // if evalContext.IsTestRun { - // return nil - // } - - //write result to db. - // cmd := &m.RecordNotificationJournalCommand{ - // OrgId: evalContext.Rule.OrgId, - // AlertId: evalContext.Rule.Id, - // NotifierId: not.GetNotifierId(), - // SentAt: time.Now().Unix(), - // Success: success, - // } - - // return bus.DispatchCtx(ctx, cmd) - return nil - }) + return n.sendAndMarkAsComplete(evalContext, notifierState) +} +func (n *notificationService) sendNotifications(evalContext *EvalContext, notifierStates NotifierStateSlice) error { + for _, notifierState := range notifierStates { + err := n.sendNotification(evalContext, notifierState) if err != nil { - n.log.Error("failed to send notification", "id", not.GetNotifierId()) + n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId()) } } @@ -143,22 +137,38 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return nil } -func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) { +func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierStateSlice, error) { query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { return nil, err } - var result []Notifier + var result NotifierStateSlice for _, notification := range query.Result { not, err := n.createNotifierFor(notification) if err != nil { - return nil, err + n.log.Error("Could not create notifier", "notifier", notification.Id) + continue } - if not.ShouldNotify(evalContext.Ctx, evalContext) { - result = append(result, not) + query := &m.GetNotificationStateQuery{ + NotifierId: notification.Id, + AlertId: evalContext.Rule.Id, + OrgId: evalContext.Rule.OrgId, + } + + err = bus.DispatchCtx(evalContext.Ctx, query) + if err != nil { + n.log.Error("Could not get notification state.", "notifier", notification.Id) + continue + } + + if not.ShouldNotify(evalContext.Ctx, evalContext, query.Result) { + result = append(result, &NotifierState{ + notifier: not, + state: query.Result, + }) } } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index f71a41235f7..e37ed92aa89 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -48,15 +48,6 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ return false } - // get last successfully sent notification - // lastNotify := time.Time{} - // for _, j := range journals { - // if j.Success { - // lastNotify = time.Unix(j.SentAt, 0) - // break - // } - // } - // Do not notify if interval has not elapsed lastNotify := time.Unix(notificationState.SentAt, 0) if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { @@ -77,7 +68,7 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ } // ShouldNotify checks this evaluation should send an alert notification -func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool { +func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext, notiferState *models.AlertNotificationState) bool { cmd := &models.GetNotificationStateQuery{ OrgId: c.Rule.OrgId, AlertId: c.Rule.Id, diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a89e777ede3..403810b772b 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -283,7 +283,7 @@ func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAl id = ? AND version = ?` - res, err := sess.Exec(sql, m.AlertNotificationStatePending, cmd.Version+1, cmd.Id, cmd.Version) + res, err := sess.Exec(sql, m.AlertNotificationStatePending, cmd.State.Version+1, cmd.State.Id, cmd.State.Version) if err != nil { return err } @@ -308,11 +308,41 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ Where("alert_notification_state.notifier_id = ?", cmd.NotifierId). Get(nj) + // if exists, return it, otherwise create it with default values if err != nil { return err } if !exist { + notificationState := &m.AlertNotificationState{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + State: "unknown", + } + + _, err := sess.Insert(notificationState) + + if err == nil { + return nil + } + + uniqenessIndexFailureCodes := []string{ + "UNIQUE constraint failed", + "pq: duplicate key value violates unique constraint", + "Error 1062: Duplicate entry ", + } + + var alreadyExists bool + + for _, code := range uniqenessIndexFailureCodes { + if strings.HasPrefix(err.Error(), code) { + alreadyExists = true + } + } + + return err + return m.ErrAlertNotificationStateNotFound } From 55e4db5cfce230432ab2cd1a15215047a4ec32bb Mon Sep 17 00:00:00 2001 From: Steve Kreitzer Date: Thu, 27 Sep 2018 19:58:07 -0400 Subject: [PATCH 250/878] Adding AWS Isolated Regions --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- public/app/plugins/datasource/cloudwatch/partials/config.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index e1e131d9f3a..ee9d9583c4e 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -235,7 +235,7 @@ func parseMultiSelectValue(input string) []string { func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { regions := []string{ "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1", - "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", + "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1", } result := make([]suggestData, 0) diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index a4df0c14807..e5ab0910cba 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -39,7 +39,7 @@
    - + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. From abefadb333760f97a77731a09cb6adbe3234eb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 28 Sep 2018 09:30:56 +0200 Subject: [PATCH 251/878] fix: preloader element issue --- public/views/index.template.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index ec51a12d34f..c39d5e08321 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -275,7 +275,10 @@ 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"; + var preloader = document.getElementsByClassName("preloader"); + if (preloader.length) { + preloader[0].className = "preloader preloader--done"; + } }; From 1a75aa54de48ade8412fd4eaf820833e4870c226 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 28 Sep 2018 10:48:08 +0200 Subject: [PATCH 252/878] wip: impl so that get alertstate also creates it if it does not exist --- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base_test.go | 41 ++++---- pkg/services/alerting/test_notification.go | 2 +- pkg/services/sqlstore/alert_notification.go | 41 +++++--- .../sqlstore/alert_notification_test.go | 97 ++++++++++--------- 5 files changed, 97 insertions(+), 86 deletions(-) diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 9826dd1dffb..2caa4d5ab58 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -46,7 +46,7 @@ type AlertmanagerNotifier struct { log log.Logger } -func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool { +func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext, notificationState *m.AlertNotificationState) bool { this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState) // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index c14006637a4..385acc39f1d 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -2,12 +2,9 @@ package notifiers import ( "context" - "errors" "testing" "time" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -126,25 +123,27 @@ func TestShouldSendAlertNotification(t *testing.T) { func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { Convey("base notifier", t, func() { - bus.ClearBusHandlers() + //bus.ClearBusHandlers() + // + //notifier := NewNotifierBase(&m.AlertNotification{ + // Id: 1, + // Name: "name", + // Type: "email", + // Settings: simplejson.New(), + //}) + //evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) + // + //Convey("should not notify query returns error", func() { + // bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetNotificationStateQuery) error { + // return errors.New("some kind of error unknown error") + // }) + // + // if notifier.ShouldNotify(context.Background(), evalContext) { + // t.Errorf("should not send notifications when query returns error") + // } + //}) - notifier := NewNotifierBase(&m.AlertNotification{ - Id: 1, - Name: "name", - Type: "email", - Settings: simplejson.New(), - }) - evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) - - Convey("should not notify query returns error", func() { - bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetNotificationStateQuery) error { - return errors.New("some kind of error unknown error") - }) - - if notifier.ShouldNotify(context.Background(), evalContext) { - t.Errorf("should not send notifications when query returns error") - } - }) + t.Error("might not need this anymore, at least not like this, control flow has changedd") }) } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index 8421360b5ed..228ec90001d 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -39,7 +39,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { return err } - return notifier.sendNotifications(createTestEvalContext(cmd), []Notifier{notifiers}) + return notifier.sendNotifications(createTestEvalContext(cmd), NotifierStateSlice{{notifier: notifiers}}) } func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 403810b772b..7af22016d73 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -302,17 +302,20 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ return withDbSession(ctx, func(sess *DBSession) error { nj := &m.AlertNotificationState{} - exist, err := sess.Desc("alert_notification_state.sent_at"). - Where("alert_notification_state.org_id = ?", cmd.OrgId). - Where("alert_notification_state.alert_id = ?", cmd.AlertId). - Where("alert_notification_state.notifier_id = ?", cmd.NotifierId). - Get(nj) + exist, err := getAlertNotificationState(sess, cmd, nj) // if exists, return it, otherwise create it with default values if err != nil { return err } + if exist { + cmd.Result = nj + return nil + } + + // normally flow ends here + if !exist { notificationState := &m.AlertNotificationState{ OrgId: cmd.OrgId, @@ -323,30 +326,38 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ _, err := sess.Insert(notificationState) - if err == nil { - return nil - } - uniqenessIndexFailureCodes := []string{ "UNIQUE constraint failed", "pq: duplicate key value violates unique constraint", "Error 1062: Duplicate entry ", } - var alreadyExists bool - for _, code := range uniqenessIndexFailureCodes { if strings.HasPrefix(err.Error(), code) { - alreadyExists = true + exist, err = getAlertNotificationState(sess, cmd, nj) + + if exist && err == nil { + cmd.Result = nj + return nil + } } } - return err - - return m.ErrAlertNotificationStateNotFound + if err != nil { + return err + } } cmd.Result = nj return nil }) } + +func getAlertNotificationState(sess *DBSession, cmd *m.GetNotificationStateQuery, nj *m.AlertNotificationState) (bool, error) { + exist, err := sess.Desc("alert_notification_state.sent_at"). + Where("alert_notification_state.org_id = ?", cmd.OrgId). + Where("alert_notification_state.alert_id = ?", cmd.AlertId). + Where("alert_notification_state.notifier_id = ?", cmd.NotifierId). + Get(nj) + return exist, err +} diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 206c96b5c6a..52682e7788f 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -1,7 +1,6 @@ package sqlstore import ( - "context" "testing" "time" @@ -14,55 +13,57 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - Convey("Alert notification state", func() { - var alertId int64 = 7 - var orgId int64 = 5 - var notifierId int64 = 10 + //Convey("Alert notification state", func() { + //var alertId int64 = 7 + //var orgId int64 = 5 + //var notifierId int64 = 10 - Convey("Getting no existant state returns error", func() { - query := &models.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - err := GetAlertNotificationState(context.Background(), query) - So(err, ShouldEqual, models.ErrAlertNotificationStateNotFound) - }) + //Convey("Getting no existant state returns error", func() { + // query := &models.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + // err := GetAlertNotificationState(context.Background(), query) + // So(err, ShouldEqual, models.ErrAlertNotificationStateNotFound) + //}) - Convey("Can insert new state for alert notifier", func() { - createCmd := &models.InsertAlertNotificationCommand{ - AlertId: alertId, - NotifierId: notifierId, - OrgId: orgId, - SentAt: 1, - State: models.AlertNotificationStateCompleted, - } - - err := InsertAlertNotificationState(context.Background(), createCmd) - So(err, ShouldBeNil) - - err = InsertAlertNotificationState(context.Background(), createCmd) - So(err, ShouldEqual, models.ErrAlertNotificationStateAlreadyExist) - - Convey("should be able to update alert notifier state", func() { - updateCmd := &models.SetAlertNotificationStateToPendingCommand{ - Id: 1, - SentAt: 1, - Version: 0, - } - - err := SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) - So(err, ShouldBeNil) - - Convey("should not be able to set pending on old version", func() { - err = SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) - So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) - }) - - Convey("should be able to set state to completed", func() { - cmd := &models.SetAlertNotificationStateToCompleteCommand{Id: 1} - err = SetAlertNotificationStateToCompleteCommand(context.Background(), cmd) - So(err, ShouldBeNil) - }) - }) - }) - }) + //Convey("Can insert new state for alert notifier", func() { + // createCmd := &models.InsertAlertNotificationCommand{ + // AlertId: alertId, + // NotifierId: notifierId, + // OrgId: orgId, + // SentAt: 1, + // State: models.AlertNotificationStateCompleted, + // } + // + // err := InsertAlertNotificationState(context.Background(), createCmd) + // So(err, ShouldBeNil) + // + // err = InsertAlertNotificationState(context.Background(), createCmd) + // So(err, ShouldEqual, models.ErrAlertNotificationStateAlreadyExist) + // + // Convey("should be able to update alert notifier state", func() { + // updateCmd := &models.SetAlertNotificationStateToPendingCommand{ + // State: models.AlertNotificationState{ + // Id: 1, + // SentAt: 1, + // Version: 0, + // } + // } + // + // err := SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) + // So(err, ShouldBeNil) + // + // Convey("should not be able to set pending on old version", func() { + // err = SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) + // So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) + // }) + // + // Convey("should be able to set state to completed", func() { + // cmd := &models.SetAlertNotificationStateToCompleteCommand{Id: 1} + // err = SetAlertNotificationStateToCompleteCommand(context.Background(), cmd) + // So(err, ShouldBeNil) + // }) + // }) + // }) + //}) Convey("Alert notifications should be empty", func() { cmd := &models.GetAlertNotificationsQuery{ From 166f93cf5445dabb67534ce1e6b72504093fa1ab Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 11:05:34 +0200 Subject: [PATCH 253/878] components, test, removed old not used files --- .../datasources/DataSourceList.test.tsx | 22 +++ .../datasources/DataSourcesActionBar.test.tsx | 23 +++ .../datasources/DataSourcesActionBar.tsx | 62 +++++++ .../features/datasources/DataSourcesList.tsx | 32 ++++ .../datasources/DataSourcesListItem.test.tsx | 20 +++ .../datasources/DataSourcesListItem.tsx | 34 ++++ .../datasources/DataSourcesListPage.test.tsx | 35 ++++ .../datasources/DataSourcesListPage.tsx | 72 +++++++++ .../datasources/__mocks__/dataSourcesMocks.ts | 45 ++++++ .../DataSourceList.test.tsx.snap | 108 +++++++++++++ .../DataSourcesActionBar.test.tsx.snap | 42 +++++ .../DataSourcesListItem.test.tsx.snap | 45 ++++++ .../DataSourcesListPage.test.tsx.snap | 152 ++++++++++++++++++ .../app/features/datasources/state/actions.ts | 51 ++++++ .../features/datasources/state/reducers.ts | 28 ++++ .../features/datasources/state/selectors.ts | 10 ++ public/app/features/plugins/all.ts | 1 - public/app/features/plugins/ds_list_ctrl.ts | 61 ------- .../features/plugins/partials/ds_list.html | 63 -------- public/app/routes/routes.ts | 8 +- public/app/store/configureStore.ts | 2 + public/app/types/datasources.ts | 17 ++ public/app/types/index.ts | 3 +- 23 files changed, 807 insertions(+), 129 deletions(-) create mode 100644 public/app/features/datasources/DataSourceList.test.tsx create mode 100644 public/app/features/datasources/DataSourcesActionBar.test.tsx create mode 100644 public/app/features/datasources/DataSourcesActionBar.tsx create mode 100644 public/app/features/datasources/DataSourcesList.tsx create mode 100644 public/app/features/datasources/DataSourcesListItem.test.tsx create mode 100644 public/app/features/datasources/DataSourcesListItem.tsx create mode 100644 public/app/features/datasources/DataSourcesListPage.test.tsx create mode 100644 public/app/features/datasources/DataSourcesListPage.tsx create mode 100644 public/app/features/datasources/__mocks__/dataSourcesMocks.ts create mode 100644 public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap create mode 100644 public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap create mode 100644 public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap create mode 100644 public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap create mode 100644 public/app/features/datasources/state/actions.ts create mode 100644 public/app/features/datasources/state/reducers.ts create mode 100644 public/app/features/datasources/state/selectors.ts delete mode 100644 public/app/features/plugins/ds_list_ctrl.ts delete mode 100644 public/app/features/plugins/partials/ds_list.html diff --git a/public/app/features/datasources/DataSourceList.test.tsx b/public/app/features/datasources/DataSourceList.test.tsx new file mode 100644 index 00000000000..6e097da2c53 --- /dev/null +++ b/public/app/features/datasources/DataSourceList.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import DataSourcesList from './DataSourcesList'; +import { getMockDataSources } from './__mocks__/dataSourcesMocks'; +import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +const setup = () => { + const props = { + dataSources: getMockDataSources(3), + layoutMode: LayoutModes.Grid, + }; + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/DataSourcesActionBar.test.tsx b/public/app/features/datasources/DataSourcesActionBar.test.tsx new file mode 100644 index 00000000000..8337271271e --- /dev/null +++ b/public/app/features/datasources/DataSourcesActionBar.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DataSourcesActionBar, Props } from './DataSourcesActionBar'; +import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +const setup = (propOverrides?: object) => { + const props: Props = { + layoutMode: LayoutModes.Grid, + searchQuery: '', + setDataSourcesLayoutMode: jest.fn(), + setDataSourcesSearchQuery: jest.fn(), + }; + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/DataSourcesActionBar.tsx b/public/app/features/datasources/DataSourcesActionBar.tsx new file mode 100644 index 00000000000..d28089b1f21 --- /dev/null +++ b/public/app/features/datasources/DataSourcesActionBar.tsx @@ -0,0 +1,62 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; +import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; +import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors'; + +export interface Props { + searchQuery: string; + layoutMode: LayoutMode; + setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; + setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; +} + +export class DataSourcesActionBar extends PureComponent { + onSearchQueryChange = event => { + this.props.setDataSourcesSearchQuery(event.target.value); + }; + + render() { + const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props; + + return ( +
    +
    + + setDataSourcesLayoutMode(mode)} + /> +
    + + ); + } +} + +function mapStateToProps(state) { + return { + searchQuery: getDataSourcesSearchQuery(state.dataSources), + layoutMode: getDataSourcesLayoutMode(state.dataSources), + }; +} + +const mapDispatchToProps = { + setDataSourcesLayoutMode, + setDataSourcesSearchQuery, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar); diff --git a/public/app/features/datasources/DataSourcesList.tsx b/public/app/features/datasources/DataSourcesList.tsx new file mode 100644 index 00000000000..4ed2203bf29 --- /dev/null +++ b/public/app/features/datasources/DataSourcesList.tsx @@ -0,0 +1,32 @@ +import React, { SFC } from 'react'; +import classNames from 'classnames/bind'; +import DataSourcesListItem from './DataSourcesListItem'; +import { DataSource } from 'app/types'; +import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +export interface Props { + dataSources: DataSource[]; + layoutMode: LayoutMode; +} + +const DataSourcesList: SFC = props => { + const { dataSources, layoutMode } = props; + + const listStyle = classNames({ + 'card-section': true, + 'card-list-layout-grid': layoutMode === LayoutModes.Grid, + 'card-list-layout-list': layoutMode === LayoutModes.List, + }); + + return ( +
    +
      + {dataSources.map((dataSource, index) => { + return ; + })} +
    +
    + ); +}; + +export default DataSourcesList; diff --git a/public/app/features/datasources/DataSourcesListItem.test.tsx b/public/app/features/datasources/DataSourcesListItem.test.tsx new file mode 100644 index 00000000000..138c71cb46a --- /dev/null +++ b/public/app/features/datasources/DataSourcesListItem.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import DataSourcesListItem from './DataSourcesListItem'; +import { getMockDataSource } from './__mocks__/dataSourcesMocks'; + +const setup = () => { + const props = { + dataSource: getMockDataSource(), + }; + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/DataSourcesListItem.tsx b/public/app/features/datasources/DataSourcesListItem.tsx new file mode 100644 index 00000000000..1bb612a299a --- /dev/null +++ b/public/app/features/datasources/DataSourcesListItem.tsx @@ -0,0 +1,34 @@ +import React, { SFC } from 'react'; +import { DataSource } from 'app/types'; + +export interface Props { + dataSource: DataSource; +} + +const DataSourcesListItem: SFC = props => { + const { dataSource } = props; + + return ( +
  • + +
    +
    {dataSource.type}
    +
    +
    +
    + +
    +
    +
    + {dataSource.name} + {dataSource.isDefault && default} +
    +
    {dataSource.url}
    +
    +
    +
    +
  • + ); +}; + +export default DataSourcesListItem; diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx new file mode 100644 index 00000000000..2cb6652ee13 --- /dev/null +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DataSourcesListPage, Props } from './DataSourcesListPage'; +import { DataSource, NavModel } from 'app/types'; +import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; +import { getMockDataSources } from './__mocks__/dataSourcesMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + dataSources: [] as DataSource[], + layoutMode: LayoutModes.Grid, + loadDataSources: jest.fn(), + navModel: {} as NavModel, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render action bar and datasources', () => { + const wrapper = setup({ + dataSources: getMockDataSources(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx new file mode 100644 index 00000000000..a6ce1be7be9 --- /dev/null +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -0,0 +1,72 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { hot } from 'react-hot-loader'; +import PageHeader from '../../core/components/PageHeader/PageHeader'; +import DataSourcesActionBar from './DataSourcesActionBar'; +import DataSourcesList from './DataSourcesList'; +import { loadDataSources } from './state/actions'; +import { getDataSources, getDataSourcesLayoutMode } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { DataSource, NavModel } from 'app/types'; +import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; +import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; + +export interface Props { + navModel: NavModel; + dataSources: DataSource[]; + layoutMode: LayoutMode; + loadDataSources: typeof loadDataSources; +} + +const emptyListModel = { + title: 'There are no data sources defined yet', + buttonIcon: 'gicon gicon-add-datasources', + buttonLink: 'datasources/new', + buttonTitle: 'Add data source', + proTip: 'You can also define data sources through configuration files.', + proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list', + proTipLinkTitle: 'Learn more', + proTipTarget: '_blank', +}; + +export class DataSourcesListPage extends PureComponent { + componentDidMount() { + this.fetchDataSources(); + } + + async fetchDataSources() { + return await this.props.loadDataSources(); + } + + render() { + const { navModel, dataSources, layoutMode } = this.props; + + if (dataSources.length === 0) { + return ; + } + + return ( +
    + +
    + + +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'datasources'), + dataSources: getDataSources(state.dataSources), + layoutMode: getDataSourcesLayoutMode(state.dataSources), + }; +} + +const mapDispatchToProps = { + loadDataSources, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage)); diff --git a/public/app/features/datasources/__mocks__/dataSourcesMocks.ts b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts new file mode 100644 index 00000000000..97819a18c82 --- /dev/null +++ b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts @@ -0,0 +1,45 @@ +import { DataSource } from 'app/types'; + +export const getMockDataSources = (amount: number): DataSource[] => { + const dataSources = []; + + for (let i = 0; i <= amount; i++) { + dataSources.push({ + access: '', + basicAuth: false, + database: `database-${i}`, + id: i, + isDefault: false, + jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' }, + name: `dataSource-${i}`, + orgId: 1, + password: '', + readOnly: false, + type: 'cloudwatch', + typeLogoUrl: 'public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png', + url: '', + user: '', + }); + } + + return dataSources; +}; + +export const getMockDataSource = (): DataSource => { + return { + access: '', + basicAuth: false, + database: '', + id: 13, + isDefault: false, + jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' }, + name: 'gdev-cloudwatch', + orgId: 1, + password: '', + readOnly: false, + type: 'cloudwatch', + typeLogoUrl: 'public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png', + url: '', + user: '', + }; +}; diff --git a/public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap new file mode 100644 index 00000000000..7167f59b048 --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
      + + + + +
    +
    +`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap new file mode 100644 index 00000000000..24f9f2126d0 --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    + + +
    + +`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap new file mode 100644 index 00000000000..a424276cf32 --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
  • + +
    +
    + cloudwatch +
    +
    +
  • +`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap new file mode 100644 index 00000000000..837a8aceb24 --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render action bar and datasources 1`] = ` +
    + +
    + + +
    +
    +`; + +exports[`Render should render component 1`] = ` + +`; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts new file mode 100644 index 00000000000..297797f2e59 --- /dev/null +++ b/public/app/features/datasources/state/actions.ts @@ -0,0 +1,51 @@ +import { ThunkAction } from 'redux-thunk'; +import { DataSource, StoreState } from 'app/types'; +import { getBackendSrv } from '../../../core/services/backend_srv'; +import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; + +export enum ActionTypes { + LoadDataSources = 'LOAD_DATA_SOURCES', + SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', + SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', +} + +export interface LoadDataSourcesAction { + type: ActionTypes.LoadDataSources; + payload: DataSource[]; +} + +export interface SetDataSourcesSearchQueryAction { + type: ActionTypes.SetDataSourcesSearchQuery; + payload: string; +} + +export interface SetDataSourcesLayoutModeAction { + type: ActionTypes.SetDataSourcesLayoutMode; + payload: LayoutMode; +} + +const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({ + type: ActionTypes.LoadDataSources, + payload: dataSources, +}); + +export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({ + type: ActionTypes.SetDataSourcesSearchQuery, + payload: searchQuery, +}); + +export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSourcesLayoutModeAction => ({ + type: ActionTypes.SetDataSourcesLayoutMode, + payload: layoutMode, +}); + +export type Action = LoadDataSourcesAction | SetDataSourcesSearchQueryAction | SetDataSourcesLayoutModeAction; + +type ThunkResult = ThunkAction; + +export function loadDataSources(): ThunkResult { + return async dispatch => { + const response = await getBackendSrv().get('/api/datasources'); + dispatch(dataSourcesLoaded(response)); + }; +} diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts new file mode 100644 index 00000000000..15604fa8b53 --- /dev/null +++ b/public/app/features/datasources/state/reducers.ts @@ -0,0 +1,28 @@ +import { DataSource, DataSourcesState } from 'app/types'; +import { Action, ActionTypes } from './actions'; +import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector'; + +const initialState: DataSourcesState = { + dataSources: [] as DataSource[], + layoutMode: LayoutModes.Grid, + searchQuery: '', +}; + +export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { + switch (action.type) { + case ActionTypes.LoadDataSources: + return { ...state, dataSources: action.payload }; + + case ActionTypes.SetDataSourcesSearchQuery: + return { ...state, searchQuery: action.payload }; + + case ActionTypes.SetDataSourcesLayoutMode: + return { ...state, layoutMode: action.payload }; + } + + return state; +}; + +export default { + dataSources: dataSourcesReducer, +}; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts new file mode 100644 index 00000000000..15ee88e715a --- /dev/null +++ b/public/app/features/datasources/state/selectors.ts @@ -0,0 +1,10 @@ +export const getDataSources = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.dataSources.filter(dataSource => { + return regex.test(dataSource.name) || regex.test(dataSource.database); + }); +}; + +export const getDataSourcesSearchQuery = state => state.searchQuery; +export const getDataSourcesLayoutMode = state => state.layoutMode; diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index 5be7593f68d..d164a6d4255 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -3,6 +3,5 @@ import './plugin_page_ctrl'; import './import_list/import_list'; import './ds_edit_ctrl'; import './ds_dashboards_ctrl'; -import './ds_list_ctrl'; import './datasource_srv'; import './plugin_component'; diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts deleted file mode 100644 index 71c1a516842..00000000000 --- a/public/app/features/plugins/ds_list_ctrl.ts +++ /dev/null @@ -1,61 +0,0 @@ -import coreModule from '../../core/core_module'; -import _ from 'lodash'; - -export class DataSourcesCtrl { - datasources: any; - unfiltered: any; - navModel: any; - searchQuery: string; - - /** @ngInject */ - constructor(private $scope, private backendSrv, private datasourceSrv, private navModelSrv) { - this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0); - backendSrv.get('/api/datasources').then(result => { - this.datasources = result; - this.unfiltered = result; - }); - } - - onQueryUpdated() { - const regex = new RegExp(this.searchQuery, 'ig'); - this.datasources = _.filter(this.unfiltered, item => { - regex.lastIndex = 0; - return regex.test(item.name) || regex.test(item.type); - }); - } - - removeDataSourceConfirmed(ds) { - this.backendSrv - .delete('/api/datasources/' + ds.id) - .then( - () => { - this.$scope.appEvent('alert-success', ['Datasource deleted', '']); - }, - () => { - this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']); - } - ) - .then(() => { - this.backendSrv.get('/api/datasources').then(result => { - this.datasources = result; - }); - this.backendSrv.get('/api/frontend/settings').then(settings => { - this.datasourceSrv.init(settings.datasources); - }); - }); - } - - removeDataSource(ds) { - this.$scope.appEvent('confirm-modal', { - title: 'Delete', - text: 'Are you sure you want to delete datasource ' + ds.name + '?', - yesText: 'Delete', - icon: 'fa-trash', - onConfirm: () => { - this.removeDataSourceConfirmed(ds); - }, - }); - } -} - -coreModule.controller('DataSourcesCtrl', DataSourcesCtrl); diff --git a/public/app/features/plugins/partials/ds_list.html b/public/app/features/plugins/partials/ds_list.html deleted file mode 100644 index fd537fc47d4..00000000000 --- a/public/app/features/plugins/partials/ds_list.html +++ /dev/null @@ -1,63 +0,0 @@ - - - diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index e4662c77367..8f17dce9757 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -8,6 +8,7 @@ import TeamList from 'app/features/teams/TeamList'; import PluginListPage from 'app/features/plugins/PluginListPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; +import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -62,9 +63,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/datasources', { - templateUrl: 'public/app/features/plugins/partials/ds_list.html', - controller: 'DataSourcesCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => DataSourcesListPage, + }, }) .when('/datasources/edit/:id', { templateUrl: 'public/app/features/plugins/partials/ds_edit.html', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 08d3d5bede0..6313bddfb3a 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; +import dataSourcesReducers from 'app/features/datasources/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, @@ -15,6 +16,7 @@ const rootReducer = combineReducers({ ...foldersReducers, ...dashboardReducers, ...pluginReducers, + ...dataSourcesReducers, }); export let store; diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 78ff7b0724c..40266fbbc5a 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -1,7 +1,24 @@ +import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector'; + export interface DataSource { id: number; orgId: number; name: string; typeLogoUrl: string; type: string; + access: string; + url: string; + password: string; + user: string; + database: string; + basicAuth: false; + isDefault: false; + jsonData: { authType: string; defaultRegion: string }; + readOnly: false; +} + +export interface DataSourcesState { + dataSources: DataSource[]; + searchQuery: string; + layoutMode: LayoutMode; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 1dd11d73564..3dbef72ce17 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -5,7 +5,7 @@ import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; -import { DataSource } from './datasources'; +import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; export { @@ -35,6 +35,7 @@ export { PluginMeta, Plugin, PluginsState, + DataSourcesState, }; export interface StoreState { From 88bbc452a7ce5dc502a675ff250072d44b0acf45 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 28 Sep 2018 11:14:36 +0200 Subject: [PATCH 254/878] wip: send and mark as complete --- pkg/models/alert_notifications.go | 5 +++-- pkg/services/alerting/notifier.go | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index b46a09ea345..5f7532576c0 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -100,8 +100,9 @@ type SetAlertNotificationStateToPendingCommand struct { } type SetAlertNotificationStateToCompleteCommand struct { - Id int64 - SentAt int64 + Id int64 + Version int64 + SentAt int64 } type GetNotificationStateQuery struct { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index d9d0e278e99..0de2abc9e0a 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,6 +3,7 @@ package alerting import ( "errors" "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -53,17 +54,27 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { } } - // get alert notification (version = 1) - // phantomjs 15 sek - // loopa notifier - ge mig ett lås! where version = 1 - // send notification - // Släpp lås - // - return n.sendNotifications(context, notifierStates) } func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *NotifierState) error { + err := notifierState.notifier.Notify(evalContext) + + cmd := &m.SetAlertNotificationStateToCompleteCommand{ + Id: notifierState.state.Id, + Version: notifierState.state.Version, + SentAt: time.Now().Unix(), + } + + err = bus.DispatchCtx(evalContext.Ctx, cmd) + if err == m.ErrAlertNotificationStateVersionConflict { + return nil + } + + if err != nil { + return err + } + return nil } From 69cc24ea3f76aafb7346333b30074107b86ad9c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 28 Sep 2018 11:17:03 +0200 Subject: [PATCH 255/878] wip: test get alert notification state --- pkg/services/sqlstore/alert_notification.go | 66 +++++++++---------- .../sqlstore/alert_notification_test.go | 30 ++++++--- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 7af22016d73..213b715c941 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -3,6 +3,7 @@ package sqlstore import ( "bytes" "context" + "errors" "fmt" "strings" "time" @@ -255,11 +256,12 @@ func InsertAlertNotificationState(ctx context.Context, cmd *m.InsertAlertNotific func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { return withDbSession(ctx, func(sess *DBSession) error { sql := `UPDATE alert_notification_state SET - state= ? + state = ?, + version = ? WHERE id = ?` - res, err := sess.Exec(sql, m.AlertNotificationStateCompleted, cmd.Id) + res, err := sess.Exec(sql, m.AlertNotificationStateCompleted, cmd.Id, cmd.Version+1) if err != nil { return err } @@ -277,7 +279,7 @@ func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetA func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToPendingCommand) error { return withDbSession(ctx, func(sess *DBSession) error { sql := `UPDATE alert_notification_state SET - state= ?, + state = ?, version = ? WHERE id = ? AND @@ -314,41 +316,33 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ return nil } - // normally flow ends here - - if !exist { - notificationState := &m.AlertNotificationState{ - OrgId: cmd.OrgId, - AlertId: cmd.AlertId, - NotifierId: cmd.NotifierId, - State: "unknown", - } - - _, err := sess.Insert(notificationState) - - uniqenessIndexFailureCodes := []string{ - "UNIQUE constraint failed", - "pq: duplicate key value violates unique constraint", - "Error 1062: Duplicate entry ", - } - - for _, code := range uniqenessIndexFailureCodes { - if strings.HasPrefix(err.Error(), code) { - exist, err = getAlertNotificationState(sess, cmd, nj) - - if exist && err == nil { - cmd.Result = nj - return nil - } - } - } - - if err != nil { - return err - } + notificationState := &m.AlertNotificationState{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + State: "unknown", } - cmd.Result = nj + if _, err := sess.Insert(notificationState); err != nil { + if dialect.IsUniqueConstraintViolation(err) { + exist, err = getAlertNotificationState(sess, cmd, nj) + + if err != nil { + return err + } + + if !exist { + return errors.New("Should not happen") + } + + cmd.Result = nj + return nil + } + + return err + } + + cmd.Result = notificationState return nil }) } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 52682e7788f..849902359cc 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -1,6 +1,7 @@ package sqlstore import ( + "context" "testing" "time" @@ -13,16 +14,27 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - //Convey("Alert notification state", func() { - //var alertId int64 = 7 - //var orgId int64 = 5 - //var notifierId int64 = 10 + Convey("Alert notification state", func() { + var alertID int64 = 7 + var orgID int64 = 5 + var notifierID int64 = 10 - //Convey("Getting no existant state returns error", func() { - // query := &models.GetNotificationStateQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - // err := GetAlertNotificationState(context.Background(), query) - // So(err, ShouldEqual, models.ErrAlertNotificationStateNotFound) - //}) + Convey("Get no existing state should create a new state", func() { + query := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err := GetAlertNotificationState(context.Background(), query) + So(err, ShouldBeNil) + So(query.Result, ShouldNotBeNil) + So(query.Result.State, ShouldEqual, "unknown") + + Convey("Get existing state should not create a new state", func() { + query2 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err := GetAlertNotificationState(context.Background(), query2) + So(err, ShouldBeNil) + So(query2.Result, ShouldNotBeNil) + So(query2.Result.Id, ShouldEqual, query.Result.Id) + }) + }) + }) //Convey("Can insert new state for alert notifier", func() { // createCmd := &models.InsertAlertNotificationCommand{ From 5bc6d857a785cc095d45e286039774431b313c6a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 11:25:32 +0200 Subject: [PATCH 256/878] stackdriver: broadcasting through $scope doesnt work anymore since query_filter_ctrl is now a sibling directive to query_aggregation_ctrl, so broadcasting is now done using $rootScope --- .../datasource/stackdriver/query_aggregation_ctrl.ts | 6 +++++- .../app/plugins/datasource/stackdriver/query_filter_ctrl.ts | 4 ++-- .../datasource/stackdriver/specs/query_filter_ctrl.test.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 50a4f630e50..92bff6b1e89 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -32,7 +32,11 @@ export class StackdriverAggregationCtrl { this.alignOptions = options.alignOptions; this.setAggOptions(); this.setAlignOptions(); - $scope.$on('metricTypeChanged', this.setAlignOptions.bind(this)); + const self = this; + $scope.$on('metricTypeChanged', () => { + self.setAggOptions(); + self.setAlignOptions(); + }); } setAlignOptions() { diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index b017f722b2c..dc3bd853463 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -41,7 +41,7 @@ export class StackdriverFilterCtrl { datasource: any; /** @ngInject */ - constructor(private $scope, private uiSegmentSrv, private templateSrv) { + constructor(private $scope, private uiSegmentSrv, private templateSrv, private $rootScope) { this.datasource = $scope.datasource; this.target = $scope.target; this.metricType = $scope.defaultDropdownValue; @@ -180,7 +180,7 @@ export class StackdriverFilterCtrl { this.target.unit = unit; this.target.valueType = valueType; this.target.metricKind = metricKind; - this.$scope.$broadcast('metricTypeChanged'); + this.$rootScope.$broadcast('metricTypeChanged'); } async getGroupBys(segment, index, removeText?: string, removeUsed = true) { diff --git a/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts index d4f295053ec..aa830e7bded 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts @@ -420,7 +420,7 @@ function createCtrlWithFakes(existingFilters?: string[]) { refresh: () => {}, }; - return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub()); + return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub(), { $broadcast: param => {} }); } function createTarget(existingFilters?: string[]) { From 7ae4076ddd3923e6b6463cef4e09c7db7f0e3090 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 11:29:18 +0200 Subject: [PATCH 257/878] added no datasources added --- .../datasources/DataSourcesListPage.test.tsx | 2 + .../datasources/DataSourcesListPage.tsx | 20 +++++---- .../DataSourcesListPage.test.tsx.snap | 42 ++++++++++++------- .../features/datasources/state/reducers.ts | 3 +- .../features/datasources/state/selectors.ts | 1 + public/app/types/datasources.ts | 1 + 6 files changed, 45 insertions(+), 24 deletions(-) diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 2cb6652ee13..fed7954d716 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -11,6 +11,7 @@ const setup = (propOverrides?: object) => { layoutMode: LayoutModes.Grid, loadDataSources: jest.fn(), navModel: {} as NavModel, + dataSourcesCount: 0, }; Object.assign(props, propOverrides); @@ -28,6 +29,7 @@ describe('Render', () => { it('should render action bar and datasources', () => { const wrapper = setup({ dataSources: getMockDataSources(5), + dataSourcesCount: 5, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index a6ce1be7be9..c6db6ee7889 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -5,7 +5,7 @@ import PageHeader from '../../core/components/PageHeader/PageHeader'; import DataSourcesActionBar from './DataSourcesActionBar'; import DataSourcesList from './DataSourcesList'; import { loadDataSources } from './state/actions'; -import { getDataSources, getDataSourcesLayoutMode } from './state/selectors'; +import { getDataSources, getDataSourcesCount, getDataSourcesLayoutMode } from './state/selectors'; import { getNavModel } from '../../core/selectors/navModel'; import { DataSource, NavModel } from 'app/types'; import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; @@ -14,6 +14,7 @@ import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; export interface Props { navModel: NavModel; dataSources: DataSource[]; + dataSourcesCount: number; layoutMode: LayoutMode; loadDataSources: typeof loadDataSources; } @@ -39,18 +40,20 @@ export class DataSourcesListPage extends PureComponent { } render() { - const { navModel, dataSources, layoutMode } = this.props; - - if (dataSources.length === 0) { - return ; - } + const { dataSources, dataSourcesCount, navModel, layoutMode } = this.props; return (
    - - + {dataSourcesCount === 0 ? ( + + ) : ( + [ + , + , + ] + )}
    ); @@ -62,6 +65,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'datasources'), dataSources: getDataSources(state.dataSources), layoutMode: getDataSourcesLayoutMode(state.dataSources), + dataSourcesCount: getDataSourcesCount(state.dataSources), }; } diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index 837a8aceb24..c19ee641e1b 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -8,7 +8,9 @@ exports[`Render should render action bar and datasources 1`] = `
    - +
    @@ -135,18 +138,27 @@ exports[`Render should render action bar and datasources 1`] = ` `; exports[`Render should render component 1`] = ` - +
    + +
    + +
    +
    `; diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 15604fa8b53..d57b0ad523a 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -6,12 +6,13 @@ const initialState: DataSourcesState = { dataSources: [] as DataSource[], layoutMode: LayoutModes.Grid, searchQuery: '', + dataSourcesCount: 0, }; export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { switch (action.type) { case ActionTypes.LoadDataSources: - return { ...state, dataSources: action.payload }; + return { ...state, dataSources: action.payload, dataSourcesCount: action.payload.length }; case ActionTypes.SetDataSourcesSearchQuery: return { ...state, searchQuery: action.payload }; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 15ee88e715a..6df08f68037 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -8,3 +8,4 @@ export const getDataSources = state => { export const getDataSourcesSearchQuery = state => state.searchQuery; export const getDataSourcesLayoutMode = state => state.layoutMode; +export const getDataSourcesCount = state => state.dataSourcesCount; diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 40266fbbc5a..b9936e7c01b 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -21,4 +21,5 @@ export interface DataSourcesState { dataSources: DataSource[]; searchQuery: string; layoutMode: LayoutMode; + dataSourcesCount: number; } From e50a87aec9f33fd7c42048df303ce3abd21c9cb6 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 12:58:01 +0200 Subject: [PATCH 258/878] using constant --- public/app/features/explore/Graph.tsx | 2 +- public/app/features/explore/__snapshots__/Graph.test.tsx.snap | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index e29abdbd194..8a9390a0b6b 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -163,7 +163,7 @@ class Graph extends Component { !this.state.showAllTimeSeries && (
    - Showing only 20 time series.{' '} + {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `} {`Show all ${ this.props.data.length }`} diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index 9f30a9e7eb9..a1e80defe92 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -468,8 +468,7 @@ exports[`Render should render component with disclaimer 1`] = ` - Showing only 20 time series. - + Showing only 20 time series. Date: Fri, 28 Sep 2018 12:59:35 +0200 Subject: [PATCH 259/878] No need to get alert notification state in ShouldNotify --- pkg/services/alerting/notifiers/base.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index e37ed92aa89..fa24f925817 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" @@ -69,19 +68,7 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ // ShouldNotify checks this evaluation should send an alert notification func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext, notiferState *models.AlertNotificationState) bool { - cmd := &models.GetNotificationStateQuery{ - OrgId: c.Rule.OrgId, - AlertId: c.Rule.Id, - NotifierId: n.Id, - } - - err := bus.DispatchCtx(ctx, cmd) - if err != nil { - n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) - return false - } - - return defaultShouldNotify(c, n.SendReminder, n.Frequency, cmd.Result) + return defaultShouldNotify(c, n.SendReminder, n.Frequency, notiferState) } func (n *NotifierBase) GetType() string { From ca50e315fa8e380ae139e2238b4d01372eb9e55e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 13:41:28 +0200 Subject: [PATCH 260/878] stackdriver: make it possible to use point values of type string --- pkg/tsdb/stackdriver/annotation_query.go | 12 ++++++++---- pkg/tsdb/stackdriver/types.go | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/stackdriver/annotation_query.go b/pkg/tsdb/stackdriver/annotation_query.go index 6345d7982ca..39cc9c341b2 100644 --- a/pkg/tsdb/stackdriver/annotation_query.go +++ b/pkg/tsdb/stackdriver/annotation_query.go @@ -3,6 +3,7 @@ package stackdriver import ( "context" "fmt" + "strconv" "strings" "time" @@ -41,12 +42,15 @@ func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, dat // reverse the order to be ascending for i := len(series.Points) - 1; i >= 0; i-- { point := series.Points[i] - + value := strconv.FormatFloat(point.Value.DoubleValue, 'f', 6, 64) + if series.ValueType == "STRING" { + value = point.Value.StringValue + } annotation := make(map[string]string) annotation["time"] = point.Interval.EndTime.UTC().Format(time.RFC3339) - annotation["title"] = formatAnnotationText(title, point.Value.DoubleValue, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) + annotation["title"] = formatAnnotationText(title, value, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) annotation["tags"] = tags - annotation["text"] = formatAnnotationText(text, point.Value.DoubleValue, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) + annotation["text"] = formatAnnotationText(text, value, series.Metric.Type, series.Metric.Labels, series.Resource.Labels) annotations = append(annotations, annotation) } } @@ -78,7 +82,7 @@ func transformAnnotationToTable(data []map[string]string, result *tsdb.QueryResu slog.Info("anno", "len", len(data)) } -func formatAnnotationText(annotationText string, pointValue float64, metricType string, metricLabels map[string]string, resourceLabels map[string]string) string { +func formatAnnotationText(annotationText string, pointValue string, metricType string, metricLabels map[string]string, resourceLabels map[string]string) string { result := legendKeyFormat.ReplaceAllFunc([]byte(annotationText), func(in []byte) []byte { metaPartName := strings.Replace(string(in), "{{", "", 1) metaPartName = strings.Replace(metaPartName, "}}", "", 1) diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index 6e3a51f1429..75e0b4c243f 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -34,6 +34,7 @@ type StackdriverResponse struct { } `json:"interval"` Value struct { DoubleValue float64 `json:"doubleValue"` + StringValue string `json:"stringValue"` } `json:"value"` } `json:"points"` } `json:"timeSeries"` From 2aae7e0c873dbdc5adb3eb4bd7e0f93ed42e0ac2 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 13:50:10 +0200 Subject: [PATCH 261/878] stackdriver: fix froamt annotation text for value --- pkg/tsdb/stackdriver/annotation_query.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/annotation_query.go b/pkg/tsdb/stackdriver/annotation_query.go index 39cc9c341b2..d86cdc5f4c1 100644 --- a/pkg/tsdb/stackdriver/annotation_query.go +++ b/pkg/tsdb/stackdriver/annotation_query.go @@ -2,7 +2,6 @@ package stackdriver import ( "context" - "fmt" "strconv" "strings" "time" @@ -99,7 +98,7 @@ func formatAnnotationText(annotationText string, pointValue string, metricType s } if metaPartName == "value" { - return []byte(fmt.Sprintf("%f", pointValue)) + return []byte(pointValue) } metaPartName = strings.Replace(metaPartName, "metric.label.", "", 1) From c1763508e0ef492b891ca8005da561022db0626d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 28 Sep 2018 14:12:26 +0200 Subject: [PATCH 262/878] handle pending and completed state for alert notifications --- pkg/models/alert_notifications.go | 4 +- pkg/services/alerting/notifier.go | 51 +++++---- pkg/services/alerting/notifiers/base_test.go | 26 ----- pkg/services/sqlstore/alert_notification.go | 21 +++- .../sqlstore/alert_notification_test.go | 100 +++++++++++------- 5 files changed, 108 insertions(+), 94 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 5f7532576c0..14bf8694207 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -100,9 +100,7 @@ type SetAlertNotificationStateToPendingCommand struct { } type SetAlertNotificationStateToCompleteCommand struct { - Id int64 - Version int64 - SentAt int64 + State *AlertNotificationState } type GetNotificationStateQuery struct { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 0de2abc9e0a..a80fb265e81 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -58,20 +59,32 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { } func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *NotifierState) error { - err := notifierState.notifier.Notify(evalContext) + not := notifierState.notifier + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - cmd := &m.SetAlertNotificationStateToCompleteCommand{ - Id: notifierState.state.Id, - Version: notifierState.state.Version, - SentAt: time.Now().Unix(), + err := not.Notify(evalContext) + + if err != nil { + n.log.Error("failed to send notification", "id", not.GetNotifierId()) + } else { + notifierState.state.SentAt = time.Now().Unix() } - err = bus.DispatchCtx(evalContext.Ctx, cmd) - if err == m.ErrAlertNotificationStateVersionConflict { + if evalContext.IsTestRun { return nil } - if err != nil { + cmd := &m.SetAlertNotificationStateToCompleteCommand{ + State: notifierState.state, + } + + if err = bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + if err == m.ErrAlertNotificationStateVersionConflict { + n.log.Error("notification state out of sync", "id", not.GetNotifierId()) + return nil + } + return err } @@ -79,19 +92,19 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no } func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *NotifierState) error { - n.log.Debug("trying to send notification", "id", notifierState.notifier.GetNotifierId()) + if !evalContext.IsTestRun { + setPendingCmd := &m.SetAlertNotificationStateToPendingCommand{ + State: notifierState.state, + } - setPendingCmd := &m.SetAlertNotificationStateToPendingCommand{ - State: notifierState.state, - } + err := bus.DispatchCtx(evalContext.Ctx, setPendingCmd) + if err == m.ErrAlertNotificationStateVersionConflict { + return nil + } - err := bus.DispatchCtx(evalContext.Ctx, setPendingCmd) - if err == m.ErrAlertNotificationStateVersionConflict { - return nil - } - - if err != nil { - return err + if err != nil { + return err + } } return n.sendAndMarkAsComplete(evalContext, notifierState) diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 385acc39f1d..50cfbef7387 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -121,32 +121,6 @@ func TestShouldSendAlertNotification(t *testing.T) { } } -func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { - Convey("base notifier", t, func() { - //bus.ClearBusHandlers() - // - //notifier := NewNotifierBase(&m.AlertNotification{ - // Id: 1, - // Name: "name", - // Type: "email", - // Settings: simplejson.New(), - //}) - //evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) - // - //Convey("should not notify query returns error", func() { - // bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetNotificationStateQuery) error { - // return errors.New("some kind of error unknown error") - // }) - // - // if notifier.ShouldNotify(context.Background(), evalContext) { - // t.Errorf("should not send notifications when query returns error") - // } - //}) - - t.Error("might not need this anymore, at least not like this, control flow has changedd") - }) -} - func TestBaseNotifier(t *testing.T) { Convey("default constructor for notifiers", t, func() { bJson := simplejson.New() diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 213b715c941..fdf467695dc 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -255,20 +255,26 @@ func InsertAlertNotificationState(ctx context.Context, cmd *m.InsertAlertNotific func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { return withDbSession(ctx, func(sess *DBSession) error { + version := cmd.State.Version + var current m.AlertNotificationState + sess.ID(cmd.State.Id).Get(¤t) + + cmd.State.State = m.AlertNotificationStateCompleted + cmd.State.Version++ + sql := `UPDATE alert_notification_state SET state = ?, version = ? WHERE id = ?` - res, err := sess.Exec(sql, m.AlertNotificationStateCompleted, cmd.Id, cmd.Version+1) + _, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.Id) + if err != nil { return err } - affected, _ := res.RowsAffected() - - if affected == 0 { + if current.Version != version { return m.ErrAlertNotificationStateVersionConflict } @@ -278,6 +284,10 @@ func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetA func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToPendingCommand) error { return withDbSession(ctx, func(sess *DBSession) error { + currentVersion := cmd.State.Version + cmd.State.State = m.AlertNotificationStatePending + cmd.State.Version++ + sql := `UPDATE alert_notification_state SET state = ?, version = ? @@ -285,7 +295,8 @@ func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAl id = ? AND version = ?` - res, err := sess.Exec(sql, m.AlertNotificationStatePending, cmd.State.Version+1, cmd.State.Id, cmd.State.Version) + res, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.Id, currentVersion) + if err != nil { return err } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 849902359cc..daed5a8cd7f 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -25,6 +25,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) So(query.Result, ShouldNotBeNil) So(query.Result.State, ShouldEqual, "unknown") + So(query.Result.Version, ShouldEqual, 0) Convey("Get existing state should not create a new state", func() { query2 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} @@ -33,50 +34,67 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(query2.Result, ShouldNotBeNil) So(query2.Result.Id, ShouldEqual, query.Result.Id) }) + + Convey("Update existing state to pending with correct version should update database", func() { + s := *query.Result + cmd := models.SetAlertNotificationStateToPendingCommand{ + State: &s, + } + err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + So(err, ShouldBeNil) + So(cmd.State.Version, ShouldEqual, 1) + So(cmd.State.State, ShouldEqual, models.AlertNotificationStatePending) + + query2 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = GetAlertNotificationState(context.Background(), query2) + So(err, ShouldBeNil) + So(query2.Result.Version, ShouldEqual, 1) + So(query2.Result.State, ShouldEqual, models.AlertNotificationStatePending) + + Convey("Update existing state to completed should update database", func() { + s := *cmd.State + cmd := models.SetAlertNotificationStateToCompleteCommand{ + State: &s, + } + err := SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd) + So(err, ShouldBeNil) + + query3 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = GetAlertNotificationState(context.Background(), query3) + So(err, ShouldBeNil) + So(query3.Result.Version, ShouldEqual, 2) + So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) + }) + + Convey("Update existing state to completed should update database, but return version mismatch", func() { + cmd.State.Version = 1000 + s := *cmd.State + cmd := models.SetAlertNotificationStateToCompleteCommand{ + State: &s, + } + err := SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd) + So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) + + query3 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = GetAlertNotificationState(context.Background(), query3) + So(err, ShouldBeNil) + So(query3.Result.Version, ShouldEqual, 1001) + So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) + }) + }) + + Convey("Update existing state to pending with incorrect version should return version mismatch error", func() { + s := *query.Result + s.Version = 1000 + cmd := models.SetAlertNotificationStateToPendingCommand{ + State: &s, + } + err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) + }) }) }) - //Convey("Can insert new state for alert notifier", func() { - // createCmd := &models.InsertAlertNotificationCommand{ - // AlertId: alertId, - // NotifierId: notifierId, - // OrgId: orgId, - // SentAt: 1, - // State: models.AlertNotificationStateCompleted, - // } - // - // err := InsertAlertNotificationState(context.Background(), createCmd) - // So(err, ShouldBeNil) - // - // err = InsertAlertNotificationState(context.Background(), createCmd) - // So(err, ShouldEqual, models.ErrAlertNotificationStateAlreadyExist) - // - // Convey("should be able to update alert notifier state", func() { - // updateCmd := &models.SetAlertNotificationStateToPendingCommand{ - // State: models.AlertNotificationState{ - // Id: 1, - // SentAt: 1, - // Version: 0, - // } - // } - // - // err := SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) - // So(err, ShouldBeNil) - // - // Convey("should not be able to set pending on old version", func() { - // err = SetAlertNotificationStateToPendingCommand(context.Background(), updateCmd) - // So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) - // }) - // - // Convey("should be able to set state to completed", func() { - // cmd := &models.SetAlertNotificationStateToCompleteCommand{Id: 1} - // err = SetAlertNotificationStateToCompleteCommand(context.Background(), cmd) - // So(err, ShouldBeNil) - // }) - // }) - // }) - //}) - Convey("Alert notifications should be empty", func() { cmd := &models.GetAlertNotificationsQuery{ OrgId: 2, From 21cfc11009e934dc1a3b7ab33b9859edff48a7af Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 14:34:58 +0200 Subject: [PATCH 263/878] implemented general actionbar --- .../components/OrgActionBar/OrgActionBar.tsx | 38 ++++++++++++ .../datasources/DataSourcesActionBar.test.tsx | 23 ------- .../datasources/DataSourcesActionBar.tsx | 62 ------------------- .../datasources/DataSourcesListPage.tsx | 45 +++++++++++--- .../features/plugins/PluginActionBar.test.tsx | 31 ---------- .../app/features/plugins/PluginActionBar.tsx | 62 ------------------- .../app/features/plugins/PluginListPage.tsx | 30 ++++++--- public/app/features/plugins/state/actions.ts | 2 +- 8 files changed, 100 insertions(+), 193 deletions(-) create mode 100644 public/app/core/components/OrgActionBar/OrgActionBar.tsx delete mode 100644 public/app/features/datasources/DataSourcesActionBar.test.tsx delete mode 100644 public/app/features/datasources/DataSourcesActionBar.tsx delete mode 100644 public/app/features/plugins/PluginActionBar.test.tsx delete mode 100644 public/app/features/plugins/PluginActionBar.tsx diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx new file mode 100644 index 00000000000..fb02985d897 --- /dev/null +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -0,0 +1,38 @@ +import React, { PureComponent } from 'react'; +import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; + +export interface Props { + searchQuery: string; + layoutMode: LayoutMode; + setLayoutMode: (mode: LayoutMode) => {}; + setSearchQuery: (value: string) => {}; + linkButton: { href: string; title: string }; +} + +export default class OrgActionBar extends PureComponent { + render() { + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; + + return ( +
    +
    + + setLayoutMode(mode)} /> +
    + + ); + } +} diff --git a/public/app/features/datasources/DataSourcesActionBar.test.tsx b/public/app/features/datasources/DataSourcesActionBar.test.tsx deleted file mode 100644 index 8337271271e..00000000000 --- a/public/app/features/datasources/DataSourcesActionBar.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DataSourcesActionBar, Props } from './DataSourcesActionBar'; -import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; - -const setup = (propOverrides?: object) => { - const props: Props = { - layoutMode: LayoutModes.Grid, - searchQuery: '', - setDataSourcesLayoutMode: jest.fn(), - setDataSourcesSearchQuery: jest.fn(), - }; - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/datasources/DataSourcesActionBar.tsx b/public/app/features/datasources/DataSourcesActionBar.tsx deleted file mode 100644 index d28089b1f21..00000000000 --- a/public/app/features/datasources/DataSourcesActionBar.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; -import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors'; - -export interface Props { - searchQuery: string; - layoutMode: LayoutMode; - setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; - setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; -} - -export class DataSourcesActionBar extends PureComponent { - onSearchQueryChange = event => { - this.props.setDataSourcesSearchQuery(event.target.value); - }; - - render() { - const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props; - - return ( -
    -
    - - setDataSourcesLayoutMode(mode)} - /> -
    - - ); - } -} - -function mapStateToProps(state) { - return { - searchQuery: getDataSourcesSearchQuery(state.dataSources), - layoutMode: getDataSourcesLayoutMode(state.dataSources), - }; -} - -const mapDispatchToProps = { - setDataSourcesLayoutMode, - setDataSourcesSearchQuery, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar); diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index c6db6ee7889..2d18d67a5d2 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -2,21 +2,29 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import PageHeader from '../../core/components/PageHeader/PageHeader'; -import DataSourcesActionBar from './DataSourcesActionBar'; +import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar'; +import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; import DataSourcesList from './DataSourcesList'; -import { loadDataSources } from './state/actions'; -import { getDataSources, getDataSourcesCount, getDataSourcesLayoutMode } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; import { DataSource, NavModel } from 'app/types'; import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; +import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { + getDataSources, + getDataSourcesCount, + getDataSourcesLayoutMode, + getDataSourcesSearchQuery, +} from './state/selectors'; export interface Props { navModel: NavModel; dataSources: DataSource[]; dataSourcesCount: number; layoutMode: LayoutMode; + searchQuery: string; loadDataSources: typeof loadDataSources; + setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; + setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; } const emptyListModel = { @@ -40,7 +48,20 @@ export class DataSourcesListPage extends PureComponent { } render() { - const { dataSources, dataSourcesCount, navModel, layoutMode } = this.props; + const { + dataSources, + dataSourcesCount, + navModel, + layoutMode, + searchQuery, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, + } = this.props; + + const linkButton = { + href: 'datasources/new', + title: 'Add data source', + }; return (
    @@ -50,7 +71,14 @@ export class DataSourcesListPage extends PureComponent { ) : ( [ - , + setDataSourcesLayoutMode(mode)} + setSearchQuery={query => setDataSourcesSearchQuery(query)} + linkButton={linkButton} + key="action-bar" + />, , ] )} @@ -66,11 +94,14 @@ function mapStateToProps(state) { dataSources: getDataSources(state.dataSources), layoutMode: getDataSourcesLayoutMode(state.dataSources), dataSourcesCount: getDataSourcesCount(state.dataSources), + searchQuery: getDataSourcesSearchQuery(state.dataSources), }; } const mapDispatchToProps = { loadDataSources, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage)); diff --git a/public/app/features/plugins/PluginActionBar.test.tsx b/public/app/features/plugins/PluginActionBar.test.tsx deleted file mode 100644 index be3f37e89fa..00000000000 --- a/public/app/features/plugins/PluginActionBar.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { PluginActionBar, Props } from './PluginActionBar'; -import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; - -const setup = (propOverrides?: object) => { - const props: Props = { - searchQuery: '', - layoutMode: LayoutModes.Grid, - setLayoutMode: jest.fn(), - setPluginsSearchQuery: jest.fn(), - }; - - Object.assign(props, propOverrides); - - const wrapper = shallow(); - const instance = wrapper.instance() as PluginActionBar; - - return { - wrapper, - instance, - }; -}; - -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/plugins/PluginActionBar.tsx b/public/app/features/plugins/PluginActionBar.tsx deleted file mode 100644 index 301b432ff5c..00000000000 --- a/public/app/features/plugins/PluginActionBar.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; -import { setLayoutMode, setPluginsSearchQuery } from './state/actions'; -import { getPluginsSearchQuery, getLayoutMode } from './state/selectors'; - -export interface Props { - searchQuery: string; - layoutMode: LayoutMode; - setLayoutMode: typeof setLayoutMode; - setPluginsSearchQuery: typeof setPluginsSearchQuery; -} - -export class PluginActionBar extends PureComponent { - onSearchQueryChange = event => { - this.props.setPluginsSearchQuery(event.target.value); - }; - - render() { - const { searchQuery, layoutMode, setLayoutMode } = this.props; - - return ( -
    -
    - - setLayoutMode(mode)} /> -
    - - ); - } -} - -function mapStateToProps(state) { - return { - searchQuery: getPluginsSearchQuery(state.plugins), - layoutMode: getLayoutMode(state.plugins), - }; -} - -const mapDispatchToProps = { - setPluginsSearchQuery, - setLayoutMode, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index de2968b126c..c549f90ebdd 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -1,20 +1,23 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import PageHeader from '../../core/components/PageHeader/PageHeader'; -import PluginActionBar from './PluginActionBar'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import PluginList from './PluginList'; -import { NavModel, Plugin } from '../../types'; -import { loadPlugins } from './state/actions'; +import { NavModel, Plugin } from 'app/types'; +import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; -import { getLayoutMode, getPlugins } from './state/selectors'; +import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors'; import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; export interface Props { navModel: NavModel; plugins: Plugin[]; layoutMode: LayoutMode; + searchQuery: string; loadPlugins: typeof loadPlugins; + setPluginsLayoutMoode: typeof setPluginsLayoutMode; + setPluginsSearchQuery: typeof setPluginsSearchQuery; } export class PluginListPage extends PureComponent { @@ -27,13 +30,23 @@ export class PluginListPage extends PureComponent { } render() { - const { navModel, plugins, layoutMode } = this.props; + const { navModel, plugins, layoutMode, setPluginsLayoutMoode, setPluginsSearchQuery, searchQuery } = this.props; + const linkButton = { + href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', + title: 'Find more plugins on Grafana.com', + }; return (
    - + setPluginsLayoutMoode(mode)} + setSearchQuery={query => setPluginsSearchQuery(query)} + linkButton={linkButton} + /> {plugins && }
    @@ -46,11 +59,14 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'plugins'), plugins: getPlugins(state.plugins), layoutMode: getLayoutMode(state.plugins), + searchQuery: getPluginsSearchQuery(state.plugins), }; } const mapDispatchToProps = { loadPlugins, + setPluginsLayoutMode, + setPluginsSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage)); diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts index 24774c6061c..dcfd510ffa0 100644 --- a/public/app/features/plugins/state/actions.ts +++ b/public/app/features/plugins/state/actions.ts @@ -24,7 +24,7 @@ export interface SetLayoutModeAction { payload: LayoutMode; } -export const setLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({ +export const setPluginsLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({ type: ActionTypes.SetLayoutMode, payload: mode, }); From d2464812eb90cc2c01e4361d74ce5a4cfbd56a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 28 Sep 2018 14:52:12 +0200 Subject: [PATCH 264/878] noop services poc --- pkg/cmd/grafana-server/server.go | 1 + .../datasources/datasource_service.go | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 pkg/services/datasources/datasource_service.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8794d7d8338..b2f4a620208 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -32,6 +32,7 @@ import ( _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" + _ "github.com/grafana/grafana/pkg/services/datasources" _ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/rendering" diff --git a/pkg/services/datasources/datasource_service.go b/pkg/services/datasources/datasource_service.go new file mode 100644 index 00000000000..2fba0bb5b87 --- /dev/null +++ b/pkg/services/datasources/datasource_service.go @@ -0,0 +1,50 @@ +package datasources + +import ( + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/setting" +) + +type DataSourceService interface { + GetById(id int64, user *models.SignedInUser) (*models.DataSource, error) +} + +type DataSourceServiceImpl struct { + log log.Logger + Cfg *setting.Cfg `inject:""` + Guardian DataSourceGuardian `inject:""` +} + +func init() { + registry.RegisterService(&DataSourceServiceImpl{}) + registry.RegisterService(&DataSourceGuardianNoop{}) +} + +func (srv *DataSourceServiceImpl) Init() error { + srv.log = log.New("datasources") + srv.log.Info("hello", "guardian", srv.Guardian.GetPermission(0, nil)) + return nil +} + +func (srv *DataSourceServiceImpl) GetById(id int64, user *models.SignedInUser) { + // check cache + // Get by id from db + // check permissions +} + +type DataSourceGuardian interface { + GetPermission(id int64, user *models.SignedInUser) bool +} + +type DataSourceGuardianNoop struct { +} + +func (dsg *DataSourceGuardianNoop) Init() error { + return nil +} + +func (dsg *DataSourceGuardianNoop) GetPermission(id int64, user *models.SignedInUser) bool { + return false +} From da856187d83e99891bcc9b43e92c5701c670fe3c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 14:57:56 +0200 Subject: [PATCH 265/878] snaps --- .../DataSourcesActionBar.test.tsx.snap | 42 ------------------- .../DataSourcesListPage.test.tsx.snap | 11 ++++- .../features/plugins/PluginListPage.test.tsx | 3 ++ .../PluginActionBar.test.tsx.snap | 40 ------------------ .../PluginListPage.test.tsx.snap | 13 +++++- 5 files changed, 25 insertions(+), 84 deletions(-) delete mode 100644 public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap delete mode 100644 public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap diff --git a/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap deleted file mode 100644 index 24f9f2126d0..00000000000 --- a/public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
    -
    - - -
    - -`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index c19ee641e1b..b50600bbc6d 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -8,8 +8,17 @@ exports[`Render should render action bar and datasources 1`] = `
    - { const props: Props = { navModel: {} as NavModel, plugins: [] as Plugin[], + searchQuery: '', + setPluginsSearchQuery: jest.fn(), + setPluginsLayoutMoode: jest.fn(), layoutMode: LayoutModes.Grid, loadPlugins: jest.fn(), }; diff --git a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap deleted file mode 100644 index 30cb53cea27..00000000000 --- a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
    -
    - - -
    - -`; diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap index 74b23d8850a..7e837d1ec7d 100644 --- a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -8,7 +8,18 @@ exports[`Render should render component 1`] = `
    - + Date: Fri, 28 Sep 2018 15:11:03 +0200 Subject: [PATCH 266/878] fix set sent_at on complete --- pkg/services/alerting/notifier.go | 2 +- pkg/services/sqlstore/alert_notification.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index a80fb265e81..4f69514977a 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -68,7 +68,7 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no if err != nil { n.log.Error("failed to send notification", "id", not.GetNotifierId()) } else { - notifierState.state.SentAt = time.Now().Unix() + notifierState.state.SentAt = time.Now().UTC().Unix() } if evalContext.IsTestRun { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index fdf467695dc..f93ef7b8164 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -264,11 +264,12 @@ func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetA sql := `UPDATE alert_notification_state SET state = ?, - version = ? + version = ?, + sent_at = ? WHERE id = ?` - _, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.Id) + _, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.SentAt, cmd.State.Id) if err != nil { return err From f384e577ddcf4faeeeab89cb17950d3033cdc10a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 15:11:33 +0200 Subject: [PATCH 267/878] stackdriver: fix reducer names --- public/app/plugins/datasource/stackdriver/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/constants.ts b/public/app/plugins/datasource/stackdriver/constants.ts index 78586a06309..628e480c3db 100644 --- a/public/app/plugins/datasource/stackdriver/constants.ts +++ b/public/app/plugins/datasource/stackdriver/constants.ts @@ -192,13 +192,13 @@ export const aggOptions = [ metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], }, { - text: 'count', + text: 'count true', value: 'REDUCE_COUNT_TRUE', valueTypes: [ValueTypes.BOOL], metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], }, { - text: 'count', + text: 'count false', value: 'REDUCE_COUNT_FALSE', valueTypes: [ValueTypes.BOOL], metricKinds: [MetricKind.GAUGE, MetricKind.DELTA], From 4abd04a5cfc5dd2ed4e9eebf01d9cb7bf15bed6a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 15:23:13 +0200 Subject: [PATCH 268/878] stackdriver: use correct default value for alignment period --- public/app/plugins/datasource/stackdriver/query_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 3dcb8df13d9..8e1f24edeb7 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -48,7 +48,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { unit: '', aggregation: { crossSeriesReducer: 'REDUCE_MEAN', - alignmentPeriod: 'auto', + alignmentPeriod: 'stackdriver-auto', perSeriesAligner: 'ALIGN_MEAN', groupBys: [], }, From 220f479ff82c59d2377586597629aff7108332cb Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 15:27:28 +0200 Subject: [PATCH 269/878] stackdriver: add support for int64 values --- pkg/tsdb/stackdriver/stackdriver.go | 10 +++++++++- pkg/tsdb/stackdriver/types.go | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 847fb271c01..0d042102ba5 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -305,7 +305,15 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta // reverse the order to be ascending for i := len(series.Points) - 1; i >= 0; i-- { point := series.Points[i] - points = append(points, tsdb.NewTimePoint(null.FloatFrom(point.Value.DoubleValue), float64((point.Interval.EndTime).Unix())*1000)) + value := point.Value.DoubleValue + if series.ValueType == "INT64" { + parsedValue, err := strconv.ParseFloat(point.Value.IntValue, 64) + if err == nil { + value = parsedValue + } + } + + points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000)) } defaultMetricName := series.Metric.Type diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index 75e0b4c243f..c58ac2968f2 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -35,6 +35,8 @@ type StackdriverResponse struct { Value struct { DoubleValue float64 `json:"doubleValue"` StringValue string `json:"stringValue"` + BoolValue bool `json:"boolValue"` + IntValue string `json:"int64Value"` } `json:"value"` } `json:"points"` } `json:"timeSeries"` From 189f89a9e429bd00707a42db2aa54b325780d173 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 15:42:02 +0200 Subject: [PATCH 270/878] stackdriver: add support for bool values --- pkg/tsdb/stackdriver/stackdriver.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 0d042102ba5..94edd0ab52d 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -306,6 +306,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta for i := len(series.Points) - 1; i >= 0; i-- { point := series.Points[i] value := point.Value.DoubleValue + if series.ValueType == "INT64" { parsedValue, err := strconv.ParseFloat(point.Value.IntValue, 64) if err == nil { @@ -313,6 +314,14 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta } } + if series.ValueType == "BOOL" { + if point.Value.BoolValue { + value = 1 + } else { + value = 0 + } + } + points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000)) } From 41060d24d5a07d2f48de2770e2895718bd47276a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 14:55:39 +0200 Subject: [PATCH 271/878] stackdriver: change pattern for annotation to metric.value --- pkg/tsdb/stackdriver/annotation_query.go | 2 +- pkg/tsdb/stackdriver/annotation_query_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/annotation_query.go b/pkg/tsdb/stackdriver/annotation_query.go index d86cdc5f4c1..db35171ad70 100644 --- a/pkg/tsdb/stackdriver/annotation_query.go +++ b/pkg/tsdb/stackdriver/annotation_query.go @@ -97,7 +97,7 @@ func formatAnnotationText(annotationText string, pointValue string, metricType s return metricPart } - if metaPartName == "value" { + if metaPartName == "metric.value" { return []byte(pointValue) } diff --git a/pkg/tsdb/stackdriver/annotation_query_test.go b/pkg/tsdb/stackdriver/annotation_query_test.go index fd7545c3759..8229470d665 100644 --- a/pkg/tsdb/stackdriver/annotation_query_test.go +++ b/pkg/tsdb/stackdriver/annotation_query_test.go @@ -19,7 +19,7 @@ func TestStackdriverAnnotationQuery(t *testing.T) { res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "annotationQuery"} query := &StackdriverQuery{} - err = executor.parseToAnnotations(res, data, query, "atitle {{metric.label.instance_name}} {{value}}", "atext {{resource.label.zone}}", "atag") + err = executor.parseToAnnotations(res, data, query, "atitle {{metric.label.instance_name}} {{metric.value}}", "atext {{resource.label.zone}}", "atag") So(err, ShouldBeNil) Convey("Should return annotations table", func() { From d11f67eb25bb766b59a3c9a5b41e3eae3a87b1df Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 16:15:57 +0200 Subject: [PATCH 272/878] stackdriver: change info logging to debug logging --- pkg/api/pluginproxy/access_token_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/pluginproxy/access_token_provider.go b/pkg/api/pluginproxy/access_token_provider.go index 8448c088de7..22407823ff9 100644 --- a/pkg/api/pluginproxy/access_token_provider.go +++ b/pkg/api/pluginproxy/access_token_provider.go @@ -111,7 +111,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data defer oauthJwtTokenCache.Unlock() if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found { if cachedToken.Expiry.After(time.Now().Add(time.Second * 10)) { - logger.Info("Using token from cache") + logger.Debug("Using token from cache") return cachedToken.AccessToken, nil } } From 31ddcdb37e99a00c1f94b42a5092bfc52af3746e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 16:20:16 +0200 Subject: [PATCH 273/878] stackdriver: make sure labels are loaded when service is changed in dropdown --- public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index dc3bd853463..173c5b801f9 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -161,6 +161,7 @@ export class StackdriverFilterCtrl { this.target.service = this.service; this.metrics = this.getMetricsList(); this.setMetricType(); + this.getLabels(); if (!this.metrics.find(m => m.value === this.target.metricType)) { this.target.metricType = this.$scope.defaultDropdownValue; } else { From fc1e2149870d3dc986713cf2261978dfd63c9d11 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 16:36:43 +0200 Subject: [PATCH 274/878] stackdriver: add relevant error message for when a user tries to create a template variable --- public/app/plugins/datasource/stackdriver/datasource.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 8a5a730eb69..c230f94c971 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -166,6 +166,10 @@ export default class StackdriverDatasource { return results; } + metricFindQuery(query) { + throw new Error('Template variables support is not yet imlemented'); + } + testDatasource() { const path = `v3/projects/${this.projectName}/metricDescriptors`; return this.doRequest(`${this.baseUrl}${path}`) From 200784ea4a31a233796a8751ad86edd7d299f3fa Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 28 Sep 2018 16:44:07 +0200 Subject: [PATCH 275/878] Explore: Store UI state in URL Storing queries, split state, and time range in URL. - harmonize query serialization when generating Explore URLs in dashboards (use of `renderUrl`) - move URL parse/serialization to Wrapper - keep UI states under two keys, one for left and one for right Explore - add option to angular router to not reload page on search change - add lots of types - fix time service function that gets triggered by URL change --- public/app/core/reducers/location.ts | 11 +- public/app/core/services/keybindingSrv.ts | 6 +- public/app/core/utils/url.ts | 9 ++ public/app/features/dashboard/time_srv.ts | 2 +- public/app/features/explore/Explore.tsx | 91 ++++++++------- public/app/features/explore/Wrapper.tsx | 108 +++++++++++++++--- public/app/features/explore/utils/query.ts | 4 +- .../app/features/panel/metrics_panel_ctrl.ts | 6 +- public/app/routes/routes.ts | 1 + public/app/types/explore.ts | 16 +++ 10 files changed, 183 insertions(+), 71 deletions(-) create mode 100644 public/app/types/explore.ts diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 6a356c4ea5a..2089cfe9f59 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -1,6 +1,6 @@ import { Action } from 'app/core/actions/location'; -import { LocationState, UrlQueryMap } from 'app/types'; -import { toUrlParams } from 'app/core/utils/url'; +import { LocationState } from 'app/types'; +import { renderUrl } from 'app/core/utils/url'; export const initialState: LocationState = { url: '', @@ -9,13 +9,6 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap | undefined): string { - if (query && Object.keys(query).length > 0) { - path += '?' + toUrlParams(query); - } - return path; -} - export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index d05e9b0c21c..a0c7cdec3cb 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; -import { encodePathComponent } from 'app/core/utils/location_util'; +import { renderUrl } from 'app/core/utils/url'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -200,8 +200,8 @@ export class KeybindingSrv { ...datasource.getExploreState(panel), range, }; - const exploreState = encodePathComponent(JSON.stringify(state)); - this.$location.url(`/explore?state=${exploreState}`); + const exploreState = JSON.stringify(state); + this.$location.url(renderUrl('/explore', { state: exploreState })); } } }); diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index 198029b0e9f..ab8be8ad222 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -2,6 +2,15 @@ * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT */ +import { UrlQueryMap } from 'app/types'; + +export function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { + path += '?' + toUrlParams(query); + } + return path; +} + export function toUrlParams(a) { const s = []; const rbracket = /\[\]$/; diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 5bf23c66bab..a96bc89daa7 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -113,7 +113,7 @@ export class TimeSrv { } private timeHasChangedSinceLoad() { - return this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to; + return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to); } setAutoRefresh(interval) { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 50d894de43f..88cc3b8cb24 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import Select from 'react-select'; +import { Query, Range, ExploreUrlState } from 'app/types/explore'; import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; import store from 'app/core/store'; import TimeSeries from 'app/core/time_series2'; -import { decodePathComponent } from 'app/core/utils/location_util'; import { parse as parseDate } from 'app/core/utils/datemath'; import ElapsedTime from './ElapsedTime'; @@ -47,37 +47,32 @@ function makeTimeSeriesList(dataList, options) { }); } -function parseUrlState(initial: string | undefined) { - if (initial) { - try { - const parsed = JSON.parse(decodePathComponent(initial)); - return { - datasource: parsed.datasource, - queries: parsed.queries.map(q => q.query), - range: parsed.range, - }; - } catch (e) { - console.error(e); - } - } - return { datasource: null, queries: [], range: DEFAULT_RANGE }; +interface ExploreProps { + datasourceSrv: any; + onChangeSplit: (split: boolean, state?: ExploreState) => void; + onSaveState: (key: string, state: ExploreState) => void; + position: string; + split: boolean; + splitState?: ExploreState; + stateKey: string; + urlState: ExploreUrlState; } -interface ExploreState { +export interface ExploreState { datasource: any; datasourceError: any; datasourceLoading: boolean | null; datasourceMissing: boolean; + datasourceName?: string; graphResult: any; history: any[]; - initialDatasource?: string; latency: number; loading: any; logsResult: any; - queries: any[]; + queries: Query[]; queryErrors: any[]; queryHints: any[]; - range: any; + range: Range; requestOptions: any; showingGraph: boolean; showingLogs: boolean; @@ -88,20 +83,21 @@ interface ExploreState { tableResult: any; } -export class Explore extends React.Component { +export class Explore extends React.Component { el: any; constructor(props) { super(props); - const initialState: ExploreState = props.initialState; - const { datasource, queries, range } = parseUrlState(props.routeParams.state); + // Split state overrides everything + const splitState: ExploreState = props.splitState; + const { datasource, queries, range } = props.urlState; this.state = { datasource: null, datasourceError: null, datasourceLoading: null, datasourceMissing: false, + datasourceName: datasource, graphResult: null, - initialDatasource: datasource, history: [], latency: 0, loading: false, @@ -118,13 +114,13 @@ export class Explore extends React.Component { supportsLogs: null, supportsTable: null, tableResult: null, - ...initialState, + ...splitState, }; } async componentDidMount() { const { datasourceSrv } = this.props; - const { initialDatasource } = this.state; + const { datasourceName } = this.state; if (!datasourceSrv) { throw new Error('No datasource service passed as props.'); } @@ -133,15 +129,15 @@ export class Explore extends React.Component { this.setState({ datasourceLoading: true }); // Priority: datasource in url, default datasource, first explore datasource let datasource; - if (initialDatasource) { - datasource = await datasourceSrv.get(initialDatasource); + if (datasourceName) { + datasource = await datasourceSrv.get(datasourceName); } else { datasource = await datasourceSrv.get(); } if (!datasource.meta.explore) { datasource = await datasourceSrv.get(datasources[0].name); } - this.setDatasource(datasource); + await this.setDatasource(datasource); } else { this.setState({ datasourceMissing: true }); } @@ -188,9 +184,14 @@ export class Explore extends React.Component { supportsLogs, supportsTable, datasourceLoading: false, + datasourceName: datasource.name, queries: nextQueries, }, - () => datasourceError === null && this.onSubmit() + () => { + if (datasourceError === null) { + this.onSubmit(); + } + } ); } @@ -220,7 +221,8 @@ export class Explore extends React.Component { queryHints: [], tableResult: null, }); - const datasource = await this.props.datasourceSrv.get(option.value); + const datasourceName = option.value; + const datasource = await this.props.datasourceSrv.get(datasourceName); this.setDatasource(datasource); }; @@ -259,21 +261,25 @@ export class Explore extends React.Component { }; onClickClear = () => { - this.setState({ - graphResult: null, - logsResult: null, - latency: 0, - queries: ensureQueries(), - queryErrors: [], - queryHints: [], - tableResult: null, - }); + this.setState( + { + graphResult: null, + logsResult: null, + latency: 0, + queries: ensureQueries(), + queryErrors: [], + queryHints: [], + tableResult: null, + }, + this.saveState + ); }; onClickCloseSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { onChangeSplit(false); + this.saveState(); } }; @@ -291,6 +297,7 @@ export class Explore extends React.Component { state.queries = state.queries.map(({ edited, ...rest }) => rest); if (onChangeSplit) { onChangeSplit(true, state); + this.saveState(); } }; @@ -349,6 +356,7 @@ export class Explore extends React.Component { if (showingLogs && supportsLogs) { this.runLogsQuery(); } + this.saveState(); }; onQuerySuccess(datasourceId: string, queries: any[]): void { @@ -471,6 +479,11 @@ export class Explore extends React.Component { return datasource.metadataRequest(url); }; + saveState = () => { + const { stateKey, onSaveState } = this.props; + onSaveState(stateKey, this.state); + }; + render() { const { datasourceSrv, position, split } = this.props; const { diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 6bdbd7cc42f..61d619ab2a7 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -1,33 +1,113 @@ import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; -import Explore from './Explore'; +import { updateLocation } from 'app/core/actions'; +import { StoreState } from 'app/types'; +import { ExploreUrlState } from 'app/types/explore'; -export default class Wrapper extends PureComponent { - state = { - initialState: null, - split: false, +import Explore, { ExploreState } from './Explore'; +import { DEFAULT_RANGE } from './TimePicker'; + +function parseUrlState(initial: string | undefined): ExploreUrlState { + if (initial) { + try { + return JSON.parse(decodeURI(initial)); + } catch (e) { + console.error(e); + } + } + return { datasource: null, queries: [], range: DEFAULT_RANGE }; +} + +function serializeStateToUrlParam(state: ExploreState): string { + const urlState: ExploreUrlState = { + datasource: state.datasourceName, + queries: state.queries.map(q => ({ query: q.query })), + range: state.range, + }; + return JSON.stringify(urlState); +} + +interface WrapperProps { + backendSrv?: any; + datasourceSrv?: any; + updateLocation: typeof updateLocation; + urlStates: { [key: string]: string }; +} + +interface WrapperState { + split: boolean; + splitState: ExploreState; +} + +const STATE_KEY_LEFT = 'state'; +const STATE_KEY_RIGHT = 'stateRight'; + +export class Wrapper extends PureComponent { + urlStates: { [key: string]: string }; + + constructor(props: WrapperProps) { + super(props); + this.urlStates = props.urlStates; + this.state = { + split: Boolean(props.urlStates[STATE_KEY_RIGHT]), + splitState: undefined, + }; + } + + onChangeSplit = (split: boolean, splitState: ExploreState) => { + this.setState({ split, splitState }); }; - handleChangeSplit = (split, initialState) => { - this.setState({ split, initialState }); + onSaveState = (key: string, state: ExploreState) => { + const urlState = serializeStateToUrlParam(state); + this.urlStates[key] = urlState; + this.props.updateLocation({ + query: this.urlStates, + }); }; render() { + const { datasourceSrv } = this.props; // State overrides for props from first Explore - const { initialState, split } = this.state; + const { split, splitState } = this.state; + const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); + const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); return (
    - - {split ? ( + + {split && ( - ) : null} + )}
    ); } } + +const mapStateToProps = (state: StoreState) => ({ + urlStates: state.location.query, +}); + +const mapDispatchToProps = { + updateLocation, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper)); diff --git a/public/app/features/explore/utils/query.ts b/public/app/features/explore/utils/query.ts index d774f619a30..4766b85f040 100644 --- a/public/app/features/explore/utils/query.ts +++ b/public/app/features/explore/utils/query.ts @@ -3,8 +3,8 @@ export function generateQueryKey(index = 0) { } export function ensureQueries(queries?) { - if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0] === 'string') { - return queries.map((query, i) => ({ key: generateQueryKey(i), query })); + if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') { + return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query })); } return [{ key: generateQueryKey(), query: '' }]; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 5eecf6036d8..c74c0716cc8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn'; import { PanelCtrl } from 'app/features/panel/panel_ctrl'; 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 { renderUrl } from 'app/core/utils/url'; import { metricsTabDirective } from './metrics_tab'; @@ -331,8 +331,8 @@ class MetricsPanelCtrl extends PanelCtrl { ...this.datasource.getExploreState(this.panel), range, }; - const exploreState = encodePathComponent(JSON.stringify(state)); - this.$location.url(`/explore?state=${exploreState}`); + const exploreState = JSON.stringify(state); + this.$location.url(renderUrl('/explore', { state: exploreState })); } addQuery(target) { diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index e4662c77367..eb67c470733 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -115,6 +115,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }) .when('/explore', { template: '', + reloadOnSearch: false, resolve: { roles: () => ['Editor', 'Admin'], component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'), diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts new file mode 100644 index 00000000000..64d65e35f3c --- /dev/null +++ b/public/app/types/explore.ts @@ -0,0 +1,16 @@ +export interface Range { + from: string; + to: string; +} + +export interface Query { + query: string; + edited?: boolean; + key?: string; +} + +export interface ExploreUrlState { + datasource: string; + queries: Query[]; + range: Range; +} From 3572692fd51c8ec0242b369921749b70ba430d31 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 16:55:39 +0200 Subject: [PATCH 276/878] stackdriver: make it impossible to select no aggregation when a group by is selected --- .../datasource/stackdriver/query_aggregation_ctrl.ts | 6 ++++++ .../app/plugins/datasource/stackdriver/query_filter_ctrl.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 92bff6b1e89..236adda6053 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -65,6 +65,12 @@ export class StackdriverAggregationCtrl { const newValue = this.aggOptions.find(o => o.value !== 'REDUCE_NONE'); this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; } + + if (this.target.aggregation.groupBys.length > 0) { + this.aggOptions = this.aggOptions.filter(o => o.value !== 'REDUCE_NONE'); + const newValue = this.aggOptions.find(o => o.value !== 'REDUCE_NONE'); + this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + } } formatAlignmentText() { diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 173c5b801f9..37625b559f0 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -241,6 +241,7 @@ export class StackdriverFilterCtrl { this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []); this.ensurePlusButton(this.groupBySegments); + this.$rootScope.$broadcast('metricTypeChanged'); this.$scope.refresh(); } From db8bbe3cad26a3bb37cba7ec8269cf594c06c8a1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 17:06:24 +0200 Subject: [PATCH 277/878] stackdriver: unit test group by and aggregation dropdown changes --- .../stackdriver/query_aggregation_ctrl.ts | 11 ++++--- .../specs/query_aggregation_ctrl.test.ts | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 236adda6053..de144071b93 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -62,14 +62,12 @@ export class StackdriverAggregationCtrl { }); if (!this.aggOptions.find(o => o.value === this.target.aggregation.crossSeriesReducer)) { - const newValue = this.aggOptions.find(o => o.value !== 'REDUCE_NONE'); - this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + this.deselectAggregationOption('REDUCE_NONE'); } if (this.target.aggregation.groupBys.length > 0) { this.aggOptions = this.aggOptions.filter(o => o.value !== 'REDUCE_NONE'); - const newValue = this.aggOptions.find(o => o.value !== 'REDUCE_NONE'); - this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + this.deselectAggregationOption('REDUCE_NONE'); } } @@ -77,6 +75,11 @@ export class StackdriverAggregationCtrl { const selectedAlignment = this.alignOptions.find(ap => ap.value === this.target.aggregation.perSeriesAligner); return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`; } + + deselectAggregationOption(notValidOptionValue: string) { + const newValue = this.aggOptions.find(o => o.value !== notValidOptionValue); + this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : ''; + } } angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index 3887381c9a8..ac9ea2ac6bc 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -4,11 +4,11 @@ describe('StackdriverAggregationCtrl', () => { let ctrl; describe('aggregation and alignment options', () => { describe('when new query result is returned from the server', () => { - describe('and result is double and gauge', () => { + describe('and result is double and gauge and no group by is used', () => { beforeEach(async () => { ctrl = new StackdriverAggregationCtrl({ $on: () => {}, - target: { valueType: 'DOUBLE', metricKind: 'GAUGE', aggregation: { crossSeriesReducer: '' } }, + target: { valueType: 'DOUBLE', metricKind: 'GAUGE', aggregation: { crossSeriesReducer: '', groupBys: [] } }, }); }); @@ -28,6 +28,33 @@ describe('StackdriverAggregationCtrl', () => { ); }); }); + + describe('and result is double and gauge and a group by is used', () => { + beforeEach(async () => { + ctrl = new StackdriverAggregationCtrl({ + $on: () => {}, + target: { + valueType: 'DOUBLE', + metricKind: 'GAUGE', + aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] }, + }, + }); + }); + + it('should populate all aggregate options except three', () => { + ctrl.setAggOptions(); + expect(ctrl.aggOptions.length).toBe(10); + expect(ctrl.aggOptions.map(o => o.value)).toEqual( + expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE', 'REDUCE_NONE']) + ); + }); + + it('should select some other reducer than REDUCE_NONE', () => { + ctrl.setAggOptions(); + expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe(''); + expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe('REDUCE_NONE'); + }); + }); }); }); }); From a25389332c3eccd1e7d919a8600ed6c78a059536 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Sep 2018 17:11:06 +0200 Subject: [PATCH 278/878] stackdriver: remove commented code --- .../datasource/stackdriver/specs/query_filter_ctrl.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts index aa830e7bded..020db584508 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts @@ -367,12 +367,6 @@ describe('StackdriverQueryFilterCtrl', () => { }); function createCtrlWithFakes(existingFilters?: string[]) { - // StackdriverFilterCtrl.prototype.panelCtrl = { - // events: { on: () => {} }, - // panel: { scopedVars: [], targets: [] }, - // refresh: () => {}, - // }; - // StackdriverFilterCtrl.prototype.target = StackdriverFilterCtrl.prototype.loadMetricDescriptors = () => { return Promise.resolve([]); }; From 399e83f91b6e999264f6b705c8861eae197926b0 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 17:15:55 +0200 Subject: [PATCH 279/878] stackdriver: remove metric.category alias pattern After discussions with the Stackdriver team, they did not think this was needed. --- pkg/tsdb/stackdriver/stackdriver.go | 27 +++++-------------- pkg/tsdb/stackdriver/stackdriver_test.go | 8 +++--- .../stackdriver/partials/query.editor.html | 3 +-- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 94edd0ab52d..d0725fca071 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -29,10 +29,9 @@ import ( ) var ( - slog log.Logger - legendKeyFormat *regexp.Regexp - longMetricNameFormat *regexp.Regexp - shortMetricNameFormat *regexp.Regexp + slog log.Logger + legendKeyFormat *regexp.Regexp + metricNameFormat *regexp.Regexp ) // StackdriverExecutor executes queries for the Stackdriver datasource @@ -58,8 +57,7 @@ func init() { slog = log.New("tsdb.stackdriver") tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor) legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) - longMetricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/([\w\d_]+)/(.+)`) - shortMetricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`) + metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`) } // Query takes in the frontend queries, parses them into the Stackdriver query format @@ -410,27 +408,16 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels func replaceWithMetricPart(metaPartName string, metricType string) []byte { // https://cloud.google.com/monitoring/api/v3/metrics-details#label_names - longMatches := longMetricNameFormat.FindStringSubmatch(metricType) - shortMatches := shortMetricNameFormat.FindStringSubmatch(metricType) + shortMatches := metricNameFormat.FindStringSubmatch(metricType) if metaPartName == "metric.name" { - if len(longMatches) > 0 { - return []byte(longMatches[3]) - } else if len(shortMatches) > 0 { + if len(shortMatches) > 0 { return []byte(shortMatches[2]) } } - if metaPartName == "metric.category" { - if len(longMatches) > 0 { - return []byte(longMatches[2]) - } - } - if metaPartName == "metric.service" { - if len(longMatches) > 0 { - return []byte(longMatches[1]) - } else if len(shortMatches) > 0 { + if len(shortMatches) > 0 { return []byte(shortMatches[1]) } } diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 022c876d178..b460de6cdbc 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -329,15 +329,15 @@ func TestStackdriver(t *testing.T) { Convey("and the alias pattern is for metric name", func() { - query := &StackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}} category {{metric.category}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}} + query := &StackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}} err = executor.parseResponse(res, data, query) So(err, ShouldBeNil) Convey("Should use alias by formatting and only show instance name", func() { So(len(res.Series), ShouldEqual, 3) - So(res.Series[0].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") - So(res.Series[1].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") - So(res.Series[2].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") + So(res.Series[0].Name, ShouldEqual, "metric instance/cpu/usage_time service compute") + So(res.Series[1].Name, ShouldEqual, "metric instance/cpu/usage_time service compute") + So(res.Series[2].Name, ShouldEqual, "metric instance/cpu/usage_time service compute") }) }) }) diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 256a854830d..1d3b6c93e76 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -49,8 +49,7 @@ Patterns: {{metric.type}} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time - {{metric.name}} = name part of metric e.g. cpu/usage_time - {{metric.category}} = category part of metric e.g. instance + {{metric.name}} = name part of metric e.g. instance/cpu/usage_time {{metric.service}} = service part of metric e.g. compute {{metric.label.label_name}} = Metric label metadata e.g. metric.label.instance_name From 8f9927660682bb42d557ed221b549c7e4a1837df Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 28 Sep 2018 17:21:00 +0200 Subject: [PATCH 280/878] first crude display --- .../components/OrgActionBar/OrgActionBar.tsx | 11 +-- .../features/plugins/PluginListPage.test.tsx | 2 +- .../app/features/plugins/PluginListPage.tsx | 7 +- public/app/features/users/UsersListPage.tsx | 66 ++++++++++++++++++ public/app/features/users/UsersTable.tsx | 67 +++++++++++++++++++ public/app/features/users/state/actions.ts | 40 +++++++++++ public/app/features/users/state/reducers.ts | 20 ++++++ public/app/features/users/state/selectors.ts | 2 + public/app/routes/routes.ts | 8 ++- public/app/store/configureStore.ts | 2 + public/app/types/index.ts | 5 ++ public/app/types/users.ts | 15 +++++ 12 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 public/app/features/users/UsersListPage.tsx create mode 100644 public/app/features/users/UsersTable.tsx create mode 100644 public/app/features/users/state/actions.ts create mode 100644 public/app/features/users/state/reducers.ts create mode 100644 public/app/features/users/state/selectors.ts create mode 100644 public/app/types/users.ts diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index fb02985d897..52d74569639 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -3,15 +3,16 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; export interface Props { searchQuery: string; - layoutMode: LayoutMode; - setLayoutMode: (mode: LayoutMode) => {}; + layoutMode?: LayoutMode; + showLayoutMode: boolean; + setLayoutMode?: (mode: LayoutMode) => {}; setSearchQuery: (value: string) => {}; linkButton: { href: string; title: string }; } export default class OrgActionBar extends PureComponent { render() { - const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; return (
    @@ -26,7 +27,9 @@ export default class OrgActionBar extends PureComponent { /> - setLayoutMode(mode)} /> + {showLayoutMode && ( + setLayoutMode(mode)} /> + )}
    diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx index 699c7d92b1e..b173ef51a2a 100644 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -10,7 +10,7 @@ const setup = (propOverrides?: object) => { plugins: [] as Plugin[], searchQuery: '', setPluginsSearchQuery: jest.fn(), - setPluginsLayoutMoode: jest.fn(), + setPluginsLayoutMode: jest.fn(), layoutMode: LayoutModes.Grid, loadPlugins: jest.fn(), }; diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index c549f90ebdd..22ff0be367f 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -16,7 +16,7 @@ export interface Props { layoutMode: LayoutMode; searchQuery: string; loadPlugins: typeof loadPlugins; - setPluginsLayoutMoode: typeof setPluginsLayoutMode; + setPluginsLayoutMode: typeof setPluginsLayoutMode; setPluginsSearchQuery: typeof setPluginsSearchQuery; } @@ -30,7 +30,7 @@ export class PluginListPage extends PureComponent { } render() { - const { navModel, plugins, layoutMode, setPluginsLayoutMoode, setPluginsSearchQuery, searchQuery } = this.props; + const { navModel, plugins, layoutMode, setPluginsLayoutMode, setPluginsSearchQuery, searchQuery } = this.props; const linkButton = { href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', @@ -42,8 +42,9 @@ export class PluginListPage extends PureComponent {
    setPluginsLayoutMoode(mode)} + setLayoutMode={mode => setPluginsLayoutMode(mode)} setSearchQuery={query => setPluginsSearchQuery(query)} linkButton={linkButton} /> diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx new file mode 100644 index 00000000000..4b935845259 --- /dev/null +++ b/public/app/features/users/UsersListPage.tsx @@ -0,0 +1,66 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import UsersTable from 'app/features/users/UsersTable'; +import { NavModel, User } from 'app/types'; +import { loadUsers, setUsersSearchQuery } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getUsers, getUsersSearchQuery } from './state/selectors'; + +export interface Props { + navModel: NavModel; + users: User[]; + searchQuery: string; + loadUsers: typeof loadUsers; + setUsersSearchQuery: typeof setUsersSearchQuery; +} + +export class UsersListPage extends PureComponent { + componentDidMount() { + this.fetchUsers(); + } + + async fetchUsers() { + return await this.props.loadUsers(); + } + render() { + const { navModel, searchQuery, setUsersSearchQuery, users } = this.props; + + const linkButton = { + href: '/org/users/add', + title: 'Add user', + }; + + return ( +
    + +
    + + +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'users'), + users: getUsers(state.users), + searchQuery: getUsersSearchQuery(state.users), + }; +} + +const mapDispatchToProps = { + loadUsers, + setUsersSearchQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage)); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx new file mode 100644 index 00000000000..38ff720472f --- /dev/null +++ b/public/app/features/users/UsersTable.tsx @@ -0,0 +1,67 @@ +import React, { SFC } from 'react'; +import { User } from 'app/types'; + +export interface Props { + users: User[]; + onRoleChange: (value: string) => {}; +} + +const UsersTable: SFC = props => { + const { users } = props; + + return ( +
    + Le Table + + + + + + + + + + {users.map((user, index) => { + return ( + + + + + + + + + ); + })} +
    + LoginEmailSeenRole +
    + + {user.login} + {user.email} + {user.lastSeenAtAge} +
    + +
    +
    +
    props.removeUser(user)} className="btn btn-danger btn-mini"> + +
    +
    +
    + ); +}; + +export default UsersTable; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts new file mode 100644 index 00000000000..0bda6b0c58a --- /dev/null +++ b/public/app/features/users/state/actions.ts @@ -0,0 +1,40 @@ +import { ThunkAction } from 'redux-thunk'; +import { StoreState } from '../../../types'; +import { getBackendSrv } from '../../../core/services/backend_srv'; +import { User } from 'app/types'; + +export enum ActionTypes { + LoadUsers = 'LOAD_USERS', + SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY', +} + +export interface LoadUsersAction { + type: ActionTypes.LoadUsers; + payload: User[]; +} + +export interface SetUsersSearchQueryAction { + type: ActionTypes.SetUsersSearchQuery; + payload: string; +} + +const usersLoaded = (users: User[]): LoadUsersAction => ({ + type: ActionTypes.LoadUsers, + payload: users, +}); + +export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({ + type: ActionTypes.SetUsersSearchQuery, + payload: query, +}); + +export type Action = LoadUsersAction | SetUsersSearchQueryAction; + +type ThunkResult = ThunkAction; + +export function loadUsers(): ThunkResult { + return async dispatch => { + const users = await getBackendSrv().get('/api/org/users'); + dispatch(usersLoaded(users)); + }; +} diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts new file mode 100644 index 00000000000..1bf62ba9d2e --- /dev/null +++ b/public/app/features/users/state/reducers.ts @@ -0,0 +1,20 @@ +import { User, UsersState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialState: UsersState = { users: [] as User[], searchQuery: '' }; + +export const usersReducer = (state = initialState, action: Action): UsersState => { + switch (action.type) { + case ActionTypes.LoadUsers: + return { ...state, users: action.payload }; + + case ActionTypes.SetUsersSearchQuery: + return { ...state, searchQuery: action.payload }; + } + + return state; +}; + +export default { + users: usersReducer, +}; diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts new file mode 100644 index 00000000000..8882c5e56e4 --- /dev/null +++ b/public/app/features/users/state/selectors.ts @@ -0,0 +1,2 @@ +export const getUsers = state => state.users; +export const getUsersSearchQuery = state => state.searchQuery; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 8f17dce9757..8a83db3e1cd 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -9,6 +9,7 @@ import PluginListPage from 'app/features/plugins/PluginListPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; +import UsersListPage from 'app/features/users/UsersListPage'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -131,9 +132,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'NewOrgCtrl', }) .when('/org/users', { - templateUrl: 'public/app/features/org/partials/orgUsers.html', - controller: 'OrgUsersCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => UsersListPage, + }, }) .when('/org/users/invite', { templateUrl: 'public/app/features/org/partials/invite.html', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 6313bddfb3a..0ca22f6988a 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -8,6 +8,7 @@ import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; +import usersReducers from 'app/features/users/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, @@ -17,6 +18,7 @@ const rootReducer = combineReducers({ ...dashboardReducers, ...pluginReducers, ...dataSourcesReducers, + ...usersReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 3dbef72ce17..f2518c2bc75 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -7,6 +7,7 @@ import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; +import { User, UsersState } from './users'; export { Team, @@ -36,6 +37,8 @@ export { Plugin, PluginsState, DataSourcesState, + User, + UsersState, }; export interface StoreState { @@ -46,4 +49,6 @@ export interface StoreState { team: TeamState; folder: FolderState; dashboard: DashboardState; + dataSources: DataSourcesState; + users: UsersState; } diff --git a/public/app/types/users.ts b/public/app/types/users.ts new file mode 100644 index 00000000000..74e7195d868 --- /dev/null +++ b/public/app/types/users.ts @@ -0,0 +1,15 @@ +export interface User { + avatarUrl: string; + email: string; + lastSeenAt: string; + lastSeenAtAge: string; + login: string; + orgId: number; + role: string; + userId: number; +} + +export interface UsersState { + users: User[]; + searchQuery: string; +} From c3e0d4205cc3071919f2b5a2ba24870428607992 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 28 Sep 2018 17:38:50 +0200 Subject: [PATCH 281/878] Make Explore a pure component --- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Wrapper.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 88cc3b8cb24..55cd44ebd8c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -83,7 +83,7 @@ export interface ExploreState { tableResult: any; } -export class Explore extends React.Component { +export class Explore extends React.PureComponent { el: any; constructor(props) { diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 61d619ab2a7..5a1b3f2831c 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; @@ -9,7 +9,7 @@ import { ExploreUrlState } from 'app/types/explore'; import Explore, { ExploreState } from './Explore'; import { DEFAULT_RANGE } from './TimePicker'; -function parseUrlState(initial: string | undefined): ExploreUrlState { +export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { return JSON.parse(decodeURI(initial)); @@ -20,7 +20,7 @@ function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource: null, queries: [], range: DEFAULT_RANGE }; } -function serializeStateToUrlParam(state: ExploreState): string { +export function serializeStateToUrlParam(state: ExploreState): string { const urlState: ExploreUrlState = { datasource: state.datasourceName, queries: state.queries.map(q => ({ query: q.query })), @@ -44,7 +44,7 @@ interface WrapperState { const STATE_KEY_LEFT = 'state'; const STATE_KEY_RIGHT = 'stateRight'; -export class Wrapper extends PureComponent { +export class Wrapper extends Component { urlStates: { [key: string]: string }; constructor(props: WrapperProps) { From 12c43d6436d156dccd95ca288456238e939372d6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 28 Sep 2018 17:39:53 +0200 Subject: [PATCH 282/878] Added test for url state in Explore --- public/app/features/explore/Wrapper.test.tsx | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 public/app/features/explore/Wrapper.test.tsx diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx new file mode 100644 index 00000000000..c71d3d384dc --- /dev/null +++ b/public/app/features/explore/Wrapper.test.tsx @@ -0,0 +1,96 @@ +import { serializeStateToUrlParam, parseUrlState } from './Wrapper'; +import { DEFAULT_RANGE } from './TimePicker'; +import { ExploreState } from './Explore'; + +const DEFAULT_EXPLORE_STATE: ExploreState = { + datasource: null, + datasourceError: null, + datasourceLoading: null, + datasourceMissing: false, + datasourceName: '', + graphResult: null, + history: [], + latency: 0, + loading: false, + logsResult: null, + queries: [], + queryErrors: [], + queryHints: [], + range: DEFAULT_RANGE, + requestOptions: null, + showingGraph: true, + showingLogs: true, + showingTable: true, + supportsGraph: null, + supportsLogs: null, + supportsTable: null, + tableResult: null, +}; + +describe('Wrapper state functions', () => { + describe('parseUrlState', () => { + it('returns default state on empty string', () => { + expect(parseUrlState('')).toMatchObject({ + datasource: null, + queries: [], + range: DEFAULT_RANGE, + }); + }); + }); + describe('serializeStateToUrlParam', () => { + it('returns url parameter value for a state object', () => { + const state = { + ...DEFAULT_EXPLORE_STATE, + datasourceName: 'foo', + range: { + from: 'now - 5h', + to: 'now', + }, + queries: [ + { + query: 'metric{test="a/b"}', + }, + { + query: 'super{foo="x/z"}', + }, + ], + }; + expect(serializeStateToUrlParam(state)).toBe( + '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' + + '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}' + ); + }); + }); + describe('interplay', () => { + it('can parse the serialized state into the original state', () => { + const state = { + ...DEFAULT_EXPLORE_STATE, + datasourceName: 'foo', + range: { + from: 'now - 5h', + to: 'now', + }, + queries: [ + { + query: 'metric{test="a/b"}', + }, + { + query: 'super{foo="x/z"}', + }, + ], + }; + const serialized = serializeStateToUrlParam(state); + const parsed = parseUrlState(serialized); + + // Account for datasource vs datasourceName + const { datasource, ...rest } = parsed; + const sameState = { + ...rest, + datasource: DEFAULT_EXPLORE_STATE.datasource, + datasourceName: datasource, + }; + + expect(state).toMatchObject(sameState); + }); + }); +}); From a1f486bbfa59d89851f295a9d4f95bd4b6ea68cd Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 18:19:57 +0200 Subject: [PATCH 283/878] stackdriver: revert an accidental commit for text template variable with dummy change in readme to be able to make a commit --- public/app/plugins/datasource/stackdriver/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/README.md b/public/app/plugins/datasource/stackdriver/README.md index 25c459d6907..6cb6f1ad4d4 100644 --- a/public/app/plugins/datasource/stackdriver/README.md +++ b/public/app/plugins/datasource/stackdriver/README.md @@ -1,4 +1,4 @@ -# Stackdriver Datasource - Native Plugin +# Stackdriver Datasource - Native Plugin Grafana ships with built-in support for Google Stackdriver. You just have to add it as a datasource and you will be ready to build dashboards for your Stackdriver metrics. From 8551ffa0b003270da3121df2eb1110d9710d172f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 28 Sep 2018 18:34:20 +0200 Subject: [PATCH 284/878] alert -> ok with reminders enabled should send --- pkg/services/alerting/notifiers/base.go | 25 ++++--- pkg/services/alerting/notifiers/base_test.go | 79 +++++++++++--------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index e1fc2969154..6dce8494569 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -51,19 +51,26 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ return false } - // Do not notify if interval has not elapsed - lastNotify := time.Unix(notificationState.SentAt, 0) - if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { - return false - } + if context.PrevAlertState == context.Rule.State && sendReminder { + // Do not notify if interval has not elapsed + lastNotify := time.Unix(notificationState.SentAt, 0) + if !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { + return false + } - // Do not notify if alert state if OK or pending even on repeated notify - if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) { - return false + // Do not notify if alert state is OK or pending even on repeated notify + if context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending { + return false + } } // Do not notify when we become OK for the first time. - if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { + if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { + return false + } + + // Do not notify when we OK -> Pending + if context.PrevAlertState == models.AlertStateOK && context.Rule.State == models.AlertStatePending { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 50cfbef7387..3645255e385 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -20,34 +20,34 @@ func TestShouldSendAlertNotification(t *testing.T) { newState m.AlertStateType sendReminder bool frequency time.Duration - journals *m.AlertNotificationState + state *m.AlertNotificationState expect bool }{ { name: "pending -> ok should not trigger an notification", - newState: m.AlertStatePending, - prevState: m.AlertStateOK, + newState: m.AlertStateOK, + prevState: m.AlertStatePending, sendReminder: false, - journals: &m.AlertNotificationState{}, + state: &m.AlertNotificationState{}, expect: false, }, { name: "ok -> alerting should trigger an notification", - newState: m.AlertStateOK, - prevState: m.AlertStateAlerting, + newState: m.AlertStateAlerting, + prevState: m.AlertStateOK, sendReminder: false, - journals: &m.AlertNotificationState{}, + state: &m.AlertNotificationState{}, expect: true, }, { name: "ok -> pending should not trigger an notification", - newState: m.AlertStateOK, - prevState: m.AlertStatePending, + newState: m.AlertStatePending, + prevState: m.AlertStateOK, sendReminder: false, - journals: &m.AlertNotificationState{}, + state: &m.AlertNotificationState{}, expect: false, }, @@ -56,66 +56,77 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: false, - journals: &m.AlertNotificationState{}, + state: &m.AlertNotificationState{}, expect: false, }, - { - name: "ok -> alerting should trigger an notification", - newState: m.AlertStateOK, - prevState: m.AlertStateAlerting, - sendReminder: true, - journals: &m.AlertNotificationState{}, - - expect: true, - }, { name: "ok -> ok with reminder should not trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: true, - journals: &m.AlertNotificationState{}, + state: &m.AlertNotificationState{}, expect: false, }, { - name: "alerting -> alerting with reminder and no journaling should trigger", - newState: m.AlertStateAlerting, + name: "alerting -> ok should trigger an notification", + newState: m.AlertStateOK, prevState: m.AlertStateAlerting, - frequency: time.Minute * 10, - sendReminder: true, - journals: &m.AlertNotificationState{}, + sendReminder: false, + state: &m.AlertNotificationState{}, expect: true, }, { - name: "alerting -> alerting with reminder and successful recent journal event should not trigger", + name: "alerting -> ok should trigger an notification when reminders enabled", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + frequency: time.Minute * 10, + sendReminder: true, + state: &m.AlertNotificationState{SentAt: tnow.Add(-time.Minute).Unix()}, + + expect: true, + }, + { + name: "alerting -> alerting with reminder and no state should trigger", newState: m.AlertStateAlerting, prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - journals: &m.AlertNotificationState{SentAt: tnow.Add(-time.Minute).Unix()}, + state: &m.AlertNotificationState{}, + + expect: true, + }, + { + name: "alerting -> alerting with reminder and last notification sent 1 minute ago should not trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateAlerting, + frequency: time.Minute * 10, + sendReminder: true, + state: &m.AlertNotificationState{SentAt: tnow.Add(-time.Minute).Unix()}, expect: false, }, { - name: "alerting -> alerting with reminder and failed recent journal event should trigger", + name: "alerting -> alerting with reminder and last notifciation sent 11 minutes ago should trigger", newState: m.AlertStateAlerting, prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - expect: true, - journals: &m.AlertNotificationState{SentAt: tnow.Add(-time.Hour).Unix()}, + state: &m.AlertNotificationState{SentAt: tnow.Add(-11 * time.Minute).Unix()}, + + expect: true, }, } for _, tc := range tcs { evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: tc.newState, + State: tc.prevState, }) - evalContext.Rule.State = tc.prevState - if defaultShouldNotify(evalContext, true, tc.frequency, tc.journals) != tc.expect { + evalContext.Rule.State = tc.newState + if defaultShouldNotify(evalContext, tc.sendReminder, tc.frequency, tc.state) != tc.expect { t.Errorf("failed test %s.\n expected \n%+v \nto return: %v", tc.name, tc, tc.expect) } } From d313ffa8470ed9c2ea8366d97e9adbf260bfb626 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 28 Sep 2018 18:35:43 +0200 Subject: [PATCH 285/878] devenv: enable some debug logging for ha test setup --- devenv/docker/ha_test/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml index 78f98ab8dc5..1ca984d68c8 100644 --- a/devenv/docker/ha_test/docker-compose.yaml +++ b/devenv/docker/ha_test/docker-compose.yaml @@ -34,6 +34,7 @@ services: - GF_DATABASE_PASSWORD=password - GF_SESSION_PROVIDER=mysql - GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(mysql:3306)/grafana?allowNativePasswords=true + - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug ports: - 3000 depends_on: From 296fd35d47d89d1f48c8f956a878fd9249732775 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 19:12:50 +0200 Subject: [PATCH 286/878] stackdriver: add help section for annotations --- .../partials/annotations.editor.html | 23 +++++++++++++++++- .../stackdriver/partials/query.editor.html | 24 +++++++++---------- public/sass/components/_infobox.scss | 9 +++++++ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html index 592dffacd51..0c2ce32f894 100644 --- a/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/annotations.editor.html @@ -1,7 +1,7 @@ -
    +
    Title @@ -14,3 +14,24 @@
    + +
    +
    Annotation Query Format
    +An annotation is an event that is overlaid on top of graphs. Annotation rendering is expensive so it is important to limit the number of rows returned. + +The Title and Text fields support templating and can use data returned from the query. For example, the Title field could have the following text: + +{{metric.type}} has value: {{metric.value}} + +Example Result: monitoring.googleapis.com/uptime_check/http_status has this value: 502 + + +{{metric.value}} = value of the metric/point +{{metric.type}} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time +{{metric.name}} = name part of metric e.g. instance/cpu/usage_time +{{metric.service}} = service part of metric e.g. compute + +{{metric.label.label_name}} = Metric label metadata e.g. metric.label.instance_name +{{resource.label.label_name}} = Resource label metadata e.g. resource.label.zone +
    +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 1d3b6c93e76..95793b6e7b1 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -40,21 +40,21 @@
    {{ctrl.lastQueryMeta.rawQueryString}}
    -
    -
    Alias Patterns
    - Format the legend keys any way you want by using alias patterns. +
    +
    Alias Patterns
    Format the legend keys any way you want by using alias patterns. - Example: {{metric.name}} - {{metric.label.instance_name}} - Result: cpu/usage_time - server1-europe-west-1 +{{metric.name}} - {{metric.label.instance_name}} - Patterns: - {{metric.type}} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time - {{metric.name}} = name part of metric e.g. instance/cpu/usage_time - {{metric.service}} = service part of metric e.g. compute +cpu/usage_time - server1-europe-west-1 - {{metric.label.label_name}} = Metric label metadata e.g. metric.label.instance_name - {{resource.label.label_name}} = Resource label metadata e.g. resource.label.zone -
    + +{{metric.type}} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time +{{metric.name}} = name part of metric e.g. instance/cpu/usage_time +{{metric.service}} = service part of metric e.g. compute + +{{metric.label.label_name}} = Metric label metadata e.g. metric.label.instance_name +{{resource.label.label_name}} = Resource label metadata e.g. resource.label.zone +
    {{ctrl.lastQueryError}}
    diff --git a/public/sass/components/_infobox.scss b/public/sass/components/_infobox.scss index 9a6a2e78a4f..52be4b4737c 100644 --- a/public/sass/components/_infobox.scss +++ b/public/sass/components/_infobox.scss @@ -19,6 +19,15 @@ padding-left: $spacer * 1.5; } + code { + @include font-family-monospace(); + font-size: $font-size-base - 2; + background-color: $code-tag-bg; + color: $text-color; + border: 1px solid $code-tag-border; + border-radius: 4px; + } + a { @extend .external-link; } From cf0189ab1a446cd06eb1f1cf500ee03b32f86d14 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 28 Sep 2018 19:17:34 +0200 Subject: [PATCH 287/878] stackdriver: no tags for annotations (yet) fixes glitch where an empty tag shows up --- public/app/plugins/datasource/stackdriver/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index c230f94c971..8ff81f3160a 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -158,7 +158,7 @@ export default class StackdriverDatasource { annotation: annotation, time: Date.parse(v[0]), title: v[1], - tags: [v[2]], + tags: [], text: v[3], }; }); From bf2abc6940e23411a2bd5ae08d9a5a5659732d28 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sat, 29 Sep 2018 23:45:28 +0200 Subject: [PATCH 288/878] stackdriver: set default view parameter to FULL --- pkg/tsdb/stackdriver/stackdriver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index d0725fca071..586e154cd5d 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -130,7 +130,7 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339)) params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339)) params.Add("filter", buildFilterString(metricType, filterParts)) - params.Add("view", query.Model.Get("view").MustString()) + params.Add("view", query.Model.Get("view").MustString("FULL")) setAggParams(¶ms, query, durationSeconds) target = params.Encode() From d412aafb7e364807b7b6a3856a31d094d33ee573 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sun, 30 Sep 2018 20:16:01 +0200 Subject: [PATCH 289/878] remove unused code --- pkg/models/alert_notifications.go | 7 ------- pkg/services/sqlstore/alert_notification.go | 23 --------------------- 2 files changed, 30 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 14bf8694207..bf52e10499a 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -111,10 +111,3 @@ type GetNotificationStateQuery struct { Result *AlertNotificationState } -type InsertAlertNotificationCommand struct { - OrgId int64 - AlertId int64 - NotifierId int64 - SentAt int64 - State AlertNotificationStateType -} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index f93ef7b8164..97db569c214 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -19,7 +19,6 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) - bus.AddHandlerCtx("sql", InsertAlertNotificationState) bus.AddHandlerCtx("sql", GetAlertNotificationState) bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand) bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand) @@ -231,28 +230,6 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { }) } -func InsertAlertNotificationState(ctx context.Context, cmd *m.InsertAlertNotificationCommand) error { - return withDbSession(ctx, func(sess *DBSession) error { - notificationState := &m.AlertNotificationState{ - OrgId: cmd.OrgId, - AlertId: cmd.AlertId, - NotifierId: cmd.NotifierId, - SentAt: cmd.SentAt, - State: cmd.State, - } - - if _, err := sess.Insert(notificationState); err != nil { - if dialect.IsUniqueConstraintViolation(err) { - return m.ErrAlertNotificationStateAlreadyExist - } - - return err - } - - return nil - }) -} - func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { return withDbSession(ctx, func(sess *DBSession) error { version := cmd.State.Version From 5ec086dc56807acd1929ba110e92812650b0ecc1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sun, 30 Sep 2018 21:52:50 +0200 Subject: [PATCH 290/878] don't notify if notification state pending If notification state is pending and last update of state was made less than a minute ago. In the case of a grafana instance is shut down/crashes between setting pending state and before sending the notification/marks as complete this logic should allow the notification to be sent after some time instead of being left in an inconsistent state where no notifications are being sent. --- pkg/models/alert_notifications.go | 2 +- pkg/services/alerting/notifiers/base.go | 10 +++++++++- pkg/services/alerting/notifiers/base_test.go | 16 ++++++++++++++++ pkg/services/sqlstore/alert_notification.go | 13 ++++++++----- pkg/services/sqlstore/alert_notification_test.go | 12 ++++++++++++ pkg/services/sqlstore/migrations/alert_mig.go | 1 + 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index bf52e10499a..8608303ddde 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -93,6 +93,7 @@ type AlertNotificationState struct { SentAt int64 State AlertNotificationStateType Version int64 + UpdatedAt int64 } type SetAlertNotificationStateToPendingCommand struct { @@ -110,4 +111,3 @@ type GetNotificationStateQuery struct { Result *AlertNotificationState } - diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 6dce8494569..b13725f1138 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -54,7 +54,7 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ if context.PrevAlertState == context.Rule.State && sendReminder { // Do not notify if interval has not elapsed lastNotify := time.Unix(notificationState.SentAt, 0) - if !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { + if notificationState.SentAt != 0 && lastNotify.Add(frequency).After(time.Now()) { return false } @@ -74,6 +74,14 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ return false } + // Do not notifu if state pending and it have been updated last minute + if notificationState.State == models.AlertNotificationStatePending { + lastUpdated := time.Unix(notificationState.UpdatedAt, 0) + if lastUpdated.Add(1 * time.Minute).After(time.Now()) { + return false + } + } + return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 3645255e385..581ff6550db 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -116,6 +116,22 @@ func TestShouldSendAlertNotification(t *testing.T) { sendReminder: true, state: &m.AlertNotificationState{SentAt: tnow.Add(-11 * time.Minute).Unix()}, + expect: true, + }, + { + name: "OK -> alerting with notifciation state pending and updated 30 seconds ago should not trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateOK, + state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-30 * time.Second).Unix()}, + + expect: false, + }, + { + name: "OK -> alerting with notifciation state pending and updated 2 minutes ago should trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateOK, + state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()}, + expect: true, }, } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 97db569c214..a69168d094a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -242,11 +242,12 @@ func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetA sql := `UPDATE alert_notification_state SET state = ?, version = ?, - sent_at = ? + sent_at = ?, + updated_at = ? WHERE id = ?` - _, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.SentAt, cmd.State.Id) + _, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.SentAt, timeNow().Unix(), cmd.State.Id) if err != nil { return err @@ -268,12 +269,13 @@ func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAl sql := `UPDATE alert_notification_state SET state = ?, - version = ? + version = ?, + updated_at = ? WHERE id = ? AND version = ?` - res, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, cmd.State.Id, currentVersion) + res, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, timeNow().Unix(), cmd.State.Id, currentVersion) if err != nil { return err @@ -310,6 +312,7 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ AlertId: cmd.AlertId, NotifierId: cmd.NotifierId, State: "unknown", + UpdatedAt: timeNow().Unix(), } if _, err := sess.Insert(notificationState); err != nil { @@ -337,7 +340,7 @@ func GetAlertNotificationState(ctx context.Context, cmd *m.GetNotificationStateQ } func getAlertNotificationState(sess *DBSession, cmd *m.GetNotificationStateQuery, nj *m.AlertNotificationState) (bool, error) { - exist, err := sess.Desc("alert_notification_state.sent_at"). + exist, err := sess. Where("alert_notification_state.org_id = ?", cmd.OrgId). Where("alert_notification_state.alert_id = ?", cmd.AlertId). Where("alert_notification_state.notifier_id = ?", cmd.NotifierId). diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index daed5a8cd7f..f82022fd18b 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -18,6 +18,9 @@ func TestAlertNotificationSQLAccess(t *testing.T) { var alertID int64 = 7 var orgID int64 = 5 var notifierID int64 = 10 + oldTimeNow := timeNow + now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC) + timeNow = func() time.Time { return now } Convey("Get no existing state should create a new state", func() { query := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} @@ -26,6 +29,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(query.Result, ShouldNotBeNil) So(query.Result.State, ShouldEqual, "unknown") So(query.Result.Version, ShouldEqual, 0) + So(query.Result.UpdatedAt, ShouldEqual, now.Unix()) Convey("Get existing state should not create a new state", func() { query2 := &models.GetNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} @@ -33,6 +37,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) So(query2.Result, ShouldNotBeNil) So(query2.Result.Id, ShouldEqual, query.Result.Id) + So(query2.Result.UpdatedAt, ShouldEqual, now.Unix()) }) Convey("Update existing state to pending with correct version should update database", func() { @@ -50,6 +55,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) So(query2.Result.Version, ShouldEqual, 1) So(query2.Result.State, ShouldEqual, models.AlertNotificationStatePending) + So(query2.Result.UpdatedAt, ShouldEqual, now.Unix()) Convey("Update existing state to completed should update database", func() { s := *cmd.State @@ -64,6 +70,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) So(query3.Result.Version, ShouldEqual, 2) So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) + So(query3.Result.UpdatedAt, ShouldEqual, now.Unix()) }) Convey("Update existing state to completed should update database, but return version mismatch", func() { @@ -80,6 +87,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldBeNil) So(query3.Result.Version, ShouldEqual, 1001) So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) + So(query3.Result.UpdatedAt, ShouldEqual, now.Unix()) }) }) @@ -93,6 +101,10 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) }) }) + + Reset(func() { + timeNow = oldTimeNow + }) }) Convey("Alert notifications should be empty", func() { diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 877dafcf1e1..bd42bb0343d 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -120,6 +120,7 @@ func addAlertMigrations(mg *Migrator) { {Name: "sent_at", Type: DB_BigInt, Nullable: false}, {Name: "state", Type: DB_NVarchar, Length: 50, Nullable: false}, {Name: "version", Type: DB_BigInt, Nullable: false}, + {Name: "updated_at", Type: DB_BigInt, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: UniqueIndex}, From 1be8fb76b8d7f71525a64b66c7b34836deca5515 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sun, 30 Sep 2018 21:57:15 +0200 Subject: [PATCH 291/878] cleanup alert_notification_state when deleting alert rules and channels --- pkg/services/sqlstore/alert.go | 4 ++++ pkg/services/sqlstore/alert_notification.go | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index ba898769578..b8206db191a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -60,6 +60,10 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro return err } + if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_id = ?", alertId); err != nil { + return err + } + return nil } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a69168d094a..1fc0394414b 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -27,8 +27,15 @@ func init() { func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { return inTransaction(func(sess *DBSession) error { sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?" - _, err := sess.Exec(sql, cmd.OrgId, cmd.Id) - return err + if _, err := sess.Exec(sql, cmd.OrgId, cmd.Id); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_notification_state.org_id = ? AND alert_notification_state.notifier_id = ?", cmd.OrgId, cmd.Id); err != nil { + return err + } + + return nil }) } From 94971abd9c789fb1acbc961e972b7d5483a31f3a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 12:01:53 +0200 Subject: [PATCH 292/878] functions and tests --- .../app/features/users/UsersListPage.test.tsx | 51 ++ public/app/features/users/UsersListPage.tsx | 32 +- public/app/features/users/UsersTable.test.tsx | 33 ++ public/app/features/users/UsersTable.tsx | 80 ++-- .../app/features/users/__mocks__/userMocks.ts | 31 ++ .../__snapshots__/UsersListPage.test.tsx.snap | 29 ++ .../__snapshots__/UsersTable.test.tsx.snap | 448 ++++++++++++++++++ public/app/features/users/state/actions.ts | 14 + 8 files changed, 677 insertions(+), 41 deletions(-) create mode 100644 public/app/features/users/UsersListPage.test.tsx create mode 100644 public/app/features/users/UsersTable.test.tsx create mode 100644 public/app/features/users/__mocks__/userMocks.ts create mode 100644 public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap create mode 100644 public/app/features/users/__snapshots__/UsersTable.test.tsx.snap diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx new file mode 100644 index 00000000000..8ba8ec4b06c --- /dev/null +++ b/public/app/features/users/UsersListPage.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { UsersListPage, Props } from './UsersListPage'; +import { NavModel, User } from 'app/types'; +import { getMockUser } from './__mocks__/userMocks'; +import appEvents from '../../core/app_events'; + +jest.mock('../../core/app_events', () => ({ + emit: jest.fn(), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + users: [] as User[], + searchQuery: '', + loadUsers: jest.fn(), + updateUser: jest.fn(), + removeUser: jest.fn(), + setUsersSearchQuery: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as UsersListPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should emit show remove user modal', () => { + const { instance } = setup(); + const mockUser = getMockUser(); + + instance.onRemoveUser(mockUser); + + expect(appEvents.emit).toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 4b935845259..88c290e80ff 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -5,7 +5,8 @@ import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import UsersTable from 'app/features/users/UsersTable'; import { NavModel, User } from 'app/types'; -import { loadUsers, setUsersSearchQuery } from './state/actions'; +import appEvents from 'app/core/app_events'; +import { loadUsers, setUsersSearchQuery, updateUser, removeUser } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; import { getUsers, getUsersSearchQuery } from './state/selectors'; @@ -15,6 +16,8 @@ export interface Props { searchQuery: string; loadUsers: typeof loadUsers; setUsersSearchQuery: typeof setUsersSearchQuery; + updateUser: typeof updateUser; + removeUser: typeof removeUser; } export class UsersListPage extends PureComponent { @@ -25,6 +28,25 @@ export class UsersListPage extends PureComponent { async fetchUsers() { return await this.props.loadUsers(); } + + onRoleChange = (role, user) => { + const updatedUser = { ...user, role: role }; + + this.props.updateUser(updatedUser); + }; + + onRemoveUser = user => { + appEvents.emit('confirm-modal', { + title: 'Delete', + text: 'Are you sure you want to delete user ' + user.login + '?', + yesText: 'Delete', + icon: 'fa-warning', + onConfirm: () => { + this.props.removeUser(user.userId); + }, + }); + }; + render() { const { navModel, searchQuery, setUsersSearchQuery, users } = this.props; @@ -43,7 +65,11 @@ export class UsersListPage extends PureComponent { setSearchQuery={setUsersSearchQuery} linkButton={linkButton} /> - + this.onRoleChange(role, user)} + onRemoveUser={user => this.onRemoveUser(user)} + />
    ); @@ -61,6 +87,8 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadUsers, setUsersSearchQuery, + updateUser, + removeUser, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage)); diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx new file mode 100644 index 00000000000..8cbfb0b4e6f --- /dev/null +++ b/public/app/features/users/UsersTable.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import UsersTable, { Props } from './UsersTable'; +import { User } from 'app/types'; +import { getMockUsers } from './__mocks__/userMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + users: [] as User[], + onRoleChange: jest.fn(), + onRemoveUser: jest.fn(), + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render users table', () => { + const wrapper = setup({ + users: getMockUsers(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 38ff720472f..e9b5edd7acb 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -3,15 +3,15 @@ import { User } from 'app/types'; export interface Props { users: User[]; - onRoleChange: (value: string) => {}; + onRoleChange: (role: string, user: User) => void; + onRemoveUser: (user: User) => void; } const UsersTable: SFC = props => { - const { users } = props; + const { users, onRoleChange, onRemoveUser } = props; return (
    - Le Table @@ -23,42 +23,44 @@ const UsersTable: SFC = props => { - {users.map((user, index) => { - return ( - - - - - - - - - ); - })} + + {users.map((user, index) => { + return ( + + + + + + + + + ); + })} +
    - - {user.login} - {user.email} - {user.lastSeenAtAge} -
    - -
    -
    -
    props.removeUser(user)} className="btn btn-danger btn-mini"> - -
    -
    + + {user.login} + {user.email} + {user.lastSeenAtAge} +
    + +
    +
    +
    onRemoveUser(user)} className="btn btn-danger btn-mini"> + +
    +
    ); diff --git a/public/app/features/users/__mocks__/userMocks.ts b/public/app/features/users/__mocks__/userMocks.ts new file mode 100644 index 00000000000..ef7789458d0 --- /dev/null +++ b/public/app/features/users/__mocks__/userMocks.ts @@ -0,0 +1,31 @@ +export const getMockUsers = (amount: number) => { + const users = []; + + for (let i = 0; i <= amount; i++) { + users.push({ + avatarUrl: 'url/to/avatar', + email: `user-${i}@test.com`, + lastSeenAt: '2018-10-01', + lastSeenAtAge: '', + login: `user-${i}`, + orgId: 1, + role: 'Admin', + userId: i, + }); + } + + return users; +}; + +export const getMockUser = () => { + return { + avatarUrl: 'url/to/avatar', + email: `user@test.com`, + lastSeenAt: '2018-10-01', + lastSeenAtAge: '', + login: `user`, + orgId: 1, + role: 'Admin', + userId: 2, + }; +}; diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap new file mode 100644 index 00000000000..689e7bd007b --- /dev/null +++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    + + +
    +
    +`; diff --git a/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap b/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap new file mode 100644 index 00000000000..9dace6a730f --- /dev/null +++ b/public/app/features/users/__snapshots__/UsersTable.test.tsx.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + + + + + + + + + + +
    + + Login + + Email + + Seen + + Role + +
    +
    +`; + +exports[`Render should render users table 1`] = ` +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + Login + + Email + + Seen + + Role + +
    + + + user-0 + + + user-0@test.com + + + +
    + +
    +
    +
    + +
    +
    + + + user-1 + + + user-1@test.com + + + +
    + +
    +
    +
    + +
    +
    + + + user-2 + + + user-2@test.com + + + +
    + +
    +
    +
    + +
    +
    + + + user-3 + + + user-3@test.com + + + +
    + +
    +
    +
    + +
    +
    + + + user-4 + + + user-4@test.com + + + +
    + +
    +
    +
    + +
    +
    + + + user-5 + + + user-5@test.com + + + +
    + +
    +
    +
    + +
    +
    +
    +`; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts index 0bda6b0c58a..2c35fb06e7a 100644 --- a/public/app/features/users/state/actions.ts +++ b/public/app/features/users/state/actions.ts @@ -38,3 +38,17 @@ export function loadUsers(): ThunkResult { dispatch(usersLoaded(users)); }; } + +export function updateUser(user: User): ThunkResult { + return async dispatch => { + await getBackendSrv().patch(`/api/org/users/${user.userId}`, user); + dispatch(loadUsers()); + }; +} + +export function removeUser(userId: number): ThunkResult { + return async dispatch => { + await getBackendSrv().delete(`/api/org/users/${userId}`); + dispatch(loadUsers()); + }; +} From 54c9beb14635b57048d8163e263feeca8b0176be Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 24 Sep 2018 17:47:43 +0200 Subject: [PATCH 293/878] Explore: jump to explore from panels with mixed datasources - extends handlers for panel menu and keypress 'x' - in a mixed-datasource panel finds first datasource that supports explore and collects its targets - passes those targets to the found datasource to be serialized for explore state - removed `supportMetrics` and `supportsExplore` - use datasource metadata instead (set in plugin.json) - Use angular timeout to wrap url change for explore jump - Extract getExploreUrl into core/utils/explore --- public/app/core/services/keybindingSrv.ts | 22 ++++---- public/app/core/utils/explore.ts | 52 +++++++++++++++++++ public/app/core/utils/location_util.ts | 5 -- .../app/features/panel/metrics_panel_ctrl.ts | 22 ++++---- .../panel/specs/metrics_panel_ctrl.test.ts | 2 +- .../datasource/cloudwatch/datasource.ts | 2 - .../plugins/datasource/influxdb/datasource.ts | 4 -- .../plugins/datasource/opentsdb/datasource.ts | 2 - .../datasource/prometheus/datasource.ts | 10 ++-- 9 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 public/app/core/utils/explore.ts diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index a0c7cdec3cb..d8dfc958dd4 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; -import { renderUrl } from 'app/core/utils/url'; +import { getExploreUrl } from 'app/core/utils/explore'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -15,7 +15,14 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { + constructor( + private $rootScope, + private $location, + private $timeout, + private datasourceSrv, + private timeSrv, + private contextSrv + ) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -194,14 +201,9 @@ export class KeybindingSrv { if (dashboard.meta.focusPanelId) { const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const datasource = await this.datasourceSrv.get(panel.datasource); - if (datasource && datasource.supportsExplore) { - const range = this.timeSrv.timeRangeForUrl(); - const state = { - ...datasource.getExploreState(panel), - range, - }; - const exploreState = JSON.stringify(state); - this.$location.url(renderUrl('/explore', { state: exploreState })); + const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv); + if (url) { + this.$timeout(() => this.$location.url(url)); } } }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts new file mode 100644 index 00000000000..cd26898259c --- /dev/null +++ b/public/app/core/utils/explore.ts @@ -0,0 +1,52 @@ +import { renderUrl } from 'app/core/utils/url'; + +/** + * Returns an Explore-URL that contains a panel's queries and the dashboard time range. + * + * @param panel Origin panel of the jump to Explore + * @param panelTargets The origin panel's query targets + * @param panelDatasource The origin panel's datasource + * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed + * @param timeSrv Time service to get the current dashboard range from + */ +export async function getExploreUrl( + panel: any, + panelTargets: any[], + panelDatasource: any, + datasourceSrv: any, + timeSrv: any +) { + let exploreDatasource = panelDatasource; + let exploreTargets = panelTargets; + let url; + + // Mixed datasources need to choose only one datasource + if (panelDatasource.meta.id === 'mixed' && panelTargets) { + // Find first explore datasource among targets + let mixedExploreDatasource; + for (const t of panel.targets) { + const datasource = await datasourceSrv.get(t.datasource); + if (datasource && datasource.meta.explore) { + mixedExploreDatasource = datasource; + break; + } + } + + // Add all its targets + if (mixedExploreDatasource) { + exploreDatasource = mixedExploreDatasource; + exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name); + } + } + + if (exploreDatasource && exploreDatasource.meta.explore) { + const range = timeSrv.timeRangeForUrl(); + const state = { + ...exploreDatasource.getExploreState(exploreTargets), + range, + }; + const exploreState = JSON.stringify(state); + url = renderUrl('/explore', { state: exploreState }); + } + return url; +} diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index 735272285ff..76f2fc5881f 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,10 +1,5 @@ import config from 'app/core/config'; -// Slash encoding for angular location provider, see https://github.com/angular/angular.js/issues/10479 -const SLASH = ''; -export const decodePathComponent = (pc: string) => decodeURIComponent(pc).replace(new RegExp(SLASH, 'g'), '/'); -export const encodePathComponent = (pc: string) => encodeURIComponent(pc.replace(/\//g, SLASH)); - export const stripBaseFromUrl = url => { const appSubUrl = config.appSubUrl; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index c74c0716cc8..b42b06f1238 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn'; import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; -import { renderUrl } from 'app/core/utils/url'; +import { getExploreUrl } from 'app/core/utils/explore'; import { metricsTabDirective } from './metrics_tab'; @@ -314,7 +314,12 @@ class MetricsPanelCtrl extends PanelCtrl { getAdditionalMenuItems() { const items = []; - if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) { + if ( + config.exploreEnabled && + this.contextSrv.isEditor && + this.datasource && + (this.datasource.meta.explore || this.datasource.meta.id === 'mixed') + ) { items.push({ text: 'Explore', click: 'ctrl.explore();', @@ -325,14 +330,11 @@ class MetricsPanelCtrl extends PanelCtrl { return items; } - explore() { - const range = this.timeSrv.timeRangeForUrl(); - const state = { - ...this.datasource.getExploreState(this.panel), - range, - }; - const exploreState = JSON.stringify(state); - this.$location.url(renderUrl('/explore', { state: exploreState })); + async explore() { + const url = await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv); + if (url) { + this.$timeout(() => this.$location.url(url)); + } } addQuery(target) { diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts index a28bf92e63b..913a2461fd0 100644 --- a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts +++ b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts @@ -38,7 +38,7 @@ describe('MetricsPanelCtrl', () => { describe('and has datasource set that supports explore and user has powers', () => { beforeEach(() => { ctrl.contextSrv = { isEditor: true }; - ctrl.datasource = { supportsExplore: true }; + ctrl.datasource = { meta: { explore: true } }; additionalItems = ctrl.getAdditionalMenuItems(); }); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 34771618095..e2b99d69df9 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -8,7 +8,6 @@ import * as templatingVariable from 'app/features/templating/variable'; export default class CloudWatchDatasource { type: any; name: any; - supportMetrics: any; proxyUrl: any; defaultRegion: any; instanceSettings: any; @@ -17,7 +16,6 @@ export default class CloudWatchDatasource { constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { this.type = 'cloudwatch'; this.name = instanceSettings.name; - this.supportMetrics = true; this.proxyUrl = instanceSettings.url; this.defaultRegion = instanceSettings.jsonData.defaultRegion; this.instanceSettings = instanceSettings; diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 5ffbf7cf418..cf9b95882bc 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -16,8 +16,6 @@ export default class InfluxDatasource { basicAuth: any; withCredentials: any; interval: any; - supportAnnotations: boolean; - supportMetrics: boolean; responseParser: any; /** @ngInject */ @@ -34,8 +32,6 @@ export default class InfluxDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = (instanceSettings.jsonData || {}).timeInterval; - this.supportAnnotations = true; - this.supportMetrics = true; this.responseParser = new ResponseParser(); } diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 7cb0806359d..772f2aa7ff9 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -10,7 +10,6 @@ export default class OpenTsDatasource { basicAuth: any; tsdbVersion: any; tsdbResolution: any; - supportMetrics: any; tagKeys: any; aggregatorsPromise: any; @@ -26,7 +25,6 @@ export default class OpenTsDatasource { instanceSettings.jsonData = instanceSettings.jsonData || {}; this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1; this.tsdbResolution = instanceSettings.jsonData.tsdbResolution || 1; - this.supportMetrics = true; this.tagKeys = {}; this.aggregatorsPromise = null; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index b53b9eb34c1..17a4b6b0f95 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -149,8 +149,6 @@ export class PrometheusDatasource { editorSrc: string; name: string; ruleMappings: { [index: string]: string }; - supportsExplore: boolean; - supportMetrics: boolean; url: string; directUrl: string; basicAuth: any; @@ -166,8 +164,6 @@ export class PrometheusDatasource { this.type = 'prometheus'; this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.name = instanceSettings.name; - this.supportsExplore = true; - this.supportMetrics = true; this.url = instanceSettings.url; this.directUrl = instanceSettings.directUrl; this.basicAuth = instanceSettings.basicAuth; @@ -522,10 +518,10 @@ export class PrometheusDatasource { }); } - getExploreState(panel) { + getExploreState(targets: any[]) { let state = {}; - if (panel.targets) { - const queries = panel.targets.map(t => ({ + if (targets && targets.length > 0) { + const queries = targets.map(t => ({ query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), format: t.format, })); From 68dfc5699b68f0d927bdc91f23f18ed314e9da3a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 1 Oct 2018 12:56:26 +0200 Subject: [PATCH 294/878] Moved explore helpers to utils/explore --- .../utils/explore.test.ts} | 9 +++--- public/app/core/utils/explore.ts | 26 ++++++++++++++++ public/app/features/explore/Explore.tsx | 30 ++----------------- public/app/features/explore/TimePicker.tsx | 1 - public/app/features/explore/Wrapper.tsx | 26 ++-------------- public/app/types/explore.ts | 25 ++++++++++++++++ 6 files changed, 61 insertions(+), 56 deletions(-) rename public/app/{features/explore/Wrapper.test.tsx => core/utils/explore.test.ts} (88%) diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/core/utils/explore.test.ts similarity index 88% rename from public/app/features/explore/Wrapper.test.tsx rename to public/app/core/utils/explore.test.ts index c71d3d384dc..8b303ffafa3 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/core/utils/explore.test.ts @@ -1,6 +1,5 @@ -import { serializeStateToUrlParam, parseUrlState } from './Wrapper'; -import { DEFAULT_RANGE } from './TimePicker'; -import { ExploreState } from './Explore'; +import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore'; +import { ExploreState } from 'app/types/explore'; const DEFAULT_EXPLORE_STATE: ExploreState = { datasource: null, @@ -27,7 +26,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { tableResult: null, }; -describe('Wrapper state functions', () => { +describe('state functions', () => { describe('parseUrlState', () => { it('returns default state on empty string', () => { expect(parseUrlState('')).toMatchObject({ @@ -57,7 +56,7 @@ describe('Wrapper state functions', () => { }; expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' + - '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}' + '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}' ); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index cd26898259c..cca841a1725 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,4 +1,10 @@ import { renderUrl } from 'app/core/utils/url'; +import { ExploreState, ExploreUrlState } from 'app/types/explore'; + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; /** * Returns an Explore-URL that contains a panel's queries and the dashboard time range. @@ -50,3 +56,23 @@ export async function getExploreUrl( } return url; } + +export function parseUrlState(initial: string | undefined): ExploreUrlState { + if (initial) { + try { + return JSON.parse(decodeURI(initial)); + } catch (e) { + console.error(e); + } + } + return { datasource: null, queries: [], range: DEFAULT_RANGE }; +} + +export function serializeStateToUrlParam(state: ExploreState): string { + const urlState: ExploreUrlState = { + datasource: state.datasourceName, + queries: state.queries.map(q => ({ query: q.query })), + range: state.range, + }; + return JSON.stringify(urlState); +} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 66e1fc0ff6b..502ad65b353 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -2,19 +2,20 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import Select from 'react-select'; -import { Query, Range, ExploreUrlState } from 'app/types/explore'; +import { ExploreState, ExploreUrlState } from 'app/types/explore'; import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; import store from 'app/core/store'; import TimeSeries from 'app/core/time_series2'; import { parse as parseDate } from 'app/core/utils/datemath'; +import { DEFAULT_RANGE } from 'app/core/utils/explore'; import ElapsedTime from './ElapsedTime'; import QueryRows from './QueryRows'; import Graph from './Graph'; import Logs from './Logs'; import Table from './Table'; -import TimePicker, { DEFAULT_RANGE } from './TimePicker'; +import TimePicker from './TimePicker'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; const MAX_HISTORY_ITEMS = 100; @@ -58,31 +59,6 @@ interface ExploreProps { urlState: ExploreUrlState; } -export interface ExploreState { - datasource: any; - datasourceError: any; - datasourceLoading: boolean | null; - datasourceMissing: boolean; - datasourceName?: string; - graphResult: any; - history: any[]; - latency: number; - loading: any; - logsResult: any; - queries: Query[]; - queryErrors: any[]; - queryHints: any[]; - range: Range; - requestOptions: any; - showingGraph: boolean; - showingLogs: boolean; - showingTable: boolean; - supportsGraph: boolean | null; - supportsLogs: boolean | null; - supportsTable: boolean | null; - tableResult: any; -} - export class Explore extends React.PureComponent { el: any; diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index 08867f8d0fc..f9c740073d0 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -5,7 +5,6 @@ import * as dateMath from 'app/core/utils/datemath'; import * as rangeUtil from 'app/core/utils/rangeutil'; const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - export const DEFAULT_RANGE = { from: 'now-6h', to: 'now', diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 5a1b3f2831c..7045910c7c4 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,31 +3,11 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; +import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreUrlState } from 'app/types/explore'; +import { ExploreState } from 'app/types/explore'; -import Explore, { ExploreState } from './Explore'; -import { DEFAULT_RANGE } from './TimePicker'; - -export function parseUrlState(initial: string | undefined): ExploreUrlState { - if (initial) { - try { - return JSON.parse(decodeURI(initial)); - } catch (e) { - console.error(e); - } - } - return { datasource: null, queries: [], range: DEFAULT_RANGE }; -} - -export function serializeStateToUrlParam(state: ExploreState): string { - const urlState: ExploreUrlState = { - datasource: state.datasourceName, - queries: state.queries.map(q => ({ query: q.query })), - range: state.range, - }; - return JSON.stringify(urlState); -} +import Explore from './Explore'; interface WrapperProps { backendSrv?: any; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 64d65e35f3c..d6ee828e3d3 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -9,6 +9,31 @@ export interface Query { key?: string; } +export interface ExploreState { + datasource: any; + datasourceError: any; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + datasourceName?: string; + graphResult: any; + history: any[]; + latency: number; + loading: any; + logsResult: any; + queries: Query[]; + queryErrors: any[]; + queryHints: any[]; + range: Range; + requestOptions: any; + showingGraph: boolean; + showingLogs: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult: any; +} + export interface ExploreUrlState { datasource: string; queries: Query[]; From 3211df7303b922d7899ed7a973039e1a3f430570 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 13:45:00 +0200 Subject: [PATCH 295/878] filter users in selector based on search --- public/app/features/users/state/selectors.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts index 8882c5e56e4..4799cba0e27 100644 --- a/public/app/features/users/state/selectors.ts +++ b/public/app/features/users/state/selectors.ts @@ -1,2 +1,9 @@ -export const getUsers = state => state.users; +export const getUsers = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.users.filter(user => { + return regex.test(user.login) || regex.test(user.email); + }); +}; + export const getUsersSearchQuery = state => state.searchQuery; From 28a9caa34d36954dcd3054f0c4a3a1d389ab25e9 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 1 Oct 2018 13:45:34 +0200 Subject: [PATCH 296/878] Update CHANGELOG.md --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fd5caf819..770edf79890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ -# 5.4.0 (unreleased) +# 5.3.0 (unreleased) + +# 5.3.0-beta2 (2018-10-01) ### New Features * **Annotations**: Enable template variables in tagged annotations queries [#9735](https://github.com/grafana/grafana/issues/9735) +* **Stackdriver**: Support for Google Stackdriver Datasource [#13289](https://github.com/grafana/grafana/pull/13289) ### Minor @@ -15,12 +18,9 @@ * **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon) * **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030) * **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173) - -# 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) +* **Alerting**: Fixes a bug where all alerts would send reminders after upgrade & restart [#13402](https://github.com/grafana/grafana/pull/13402) +* **Alerting**: Concurrent render limit for graphs used in notifications [#13401](https://github.com/grafana/grafana/pull/13401) * **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 a43ede70bc9268aed64a9df0240157bf8ccab9d1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 14:02:13 +0200 Subject: [PATCH 297/878] added default prop instead of specifying prop --- public/app/core/components/OrgActionBar/OrgActionBar.tsx | 4 ++++ public/app/features/datasources/DataSourcesListPage.test.tsx | 3 +++ public/app/features/plugins/PluginListPage.tsx | 1 - 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index 52d74569639..f3112ffaefd 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -11,6 +11,10 @@ export interface Props { } export default class OrgActionBar extends PureComponent { + static defaultProps = { + showLayoutMode: true, + }; + render() { const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index fed7954d716..96f6c304b16 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -12,6 +12,9 @@ const setup = (propOverrides?: object) => { loadDataSources: jest.fn(), navModel: {} as NavModel, dataSourcesCount: 0, + searchQuery: '', + setDataSourcesSearchQuery: jest.fn(), + setDataSourcesLayoutMode: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index 22ff0be367f..c24d44d6826 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -42,7 +42,6 @@ export class PluginListPage extends PureComponent {
    setPluginsLayoutMode(mode)} setSearchQuery={query => setPluginsSearchQuery(query)} From 13666c8462b4f25f11496dc6e834a593050d4eea Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 14:17:28 +0200 Subject: [PATCH 298/878] tests --- .../OrgActionBar/OrgActionBar.test.tsx | 32 ++++++++ .../__snapshots__/OrgActionBar.test.tsx.snap | 74 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 public/app/core/components/OrgActionBar/OrgActionBar.test.tsx create mode 100644 public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx new file mode 100644 index 00000000000..d1edeeaa779 --- /dev/null +++ b/public/app/core/components/OrgActionBar/OrgActionBar.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import OrgActionBar, { Props } from './OrgActionBar'; + +const setup = (propOverrides?: object) => { + const props: Props = { + searchQuery: '', + showLayoutMode: true, + setSearchQuery: jest.fn(), + linkButton: { href: 'some/url', title: 'test' }, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should hide layout mode', () => { + const wrapper = setup({ + showLayoutMode: false, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap new file mode 100644 index 00000000000..9fdae04975d --- /dev/null +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should hide layout mode 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Render should render component 1`] = ` +
    +
    + + +
    + +`; From b29ac5c509238ec88e96badf4828fc8c143cbe6c Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 1 Oct 2018 14:35:20 +0200 Subject: [PATCH 299/878] docs: stackdriver version notice. --- docs/sources/features/datasources/stackdriver.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 890972376c2..96f3ba3382e 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -12,7 +12,10 @@ weight = 11 # Using Google Stackdriver in Grafana -Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics. It is only available in Grafana 5.3+. The datasource is currently a beta feature and is subject to change. +> Only available in Grafana v5.3+. +> The datasource is currently a beta feature and is subject to change. + +Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics. ## Adding the data source to Grafana From b3c78f1265d3fdd35768fd54adff85917e91531f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Oct 2018 15:38:55 +0200 Subject: [PATCH 300/878] wip: data source permissions hooks --- pkg/api/api.go | 6 +-- pkg/api/datasources.go | 34 ++++++++++--- pkg/cmd/grafana-server/server.go | 1 - pkg/models/datasource.go | 26 +++++++++- .../datasources/datasource_service.go | 50 ------------------- pkg/services/sqlstore/datasource.go | 1 + 6 files changed, 55 insertions(+), 63 deletions(-) delete mode 100644 pkg/services/datasources/datasource_service.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 39b332aeb9f..dcbc3a7c58f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() { datasourceRoute.Get("/", Wrap(GetDataSources)) datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource)) datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource)) - datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID)) + datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById)) datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName)) - datasourceRoute.Get("/:id", Wrap(GetDataSourceByID)) + datasourceRoute.Get("/:id", Wrap(GetDataSourceById)) datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName)) }, reqOrgAdmin) - apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn) + apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn) apiRoute.Get("/plugins", Wrap(GetPluginList)) apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID)) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 23dbb221d71..b1b13d7abfd 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -20,8 +20,8 @@ func GetDataSources(c *m.ReqContext) Response { result := make(dtos.DataSourceList, 0) for _, ds := range query.Result { dsItem := dtos.DataSourceListItemDTO{ - Id: ds.Id, OrgId: ds.OrgId, + Id: ds.Id, Name: ds.Name, Url: ds.Url, Type: ds.Type, @@ -49,7 +49,27 @@ func GetDataSources(c *m.ReqContext) Response { return JSON(200, &result) } -func GetDataSourceByID(c *m.ReqContext) Response { +func hasRequiredDatasourcePermission(dsId int64, permission m.DataSourcePermissionType, user *m.SignedInUser) Response { + query := m.HasRequiredDataSourcePermissionQuery{ + Id: dsId, + User: user, + RequiredPermission: permission, + } + + if err := bus.Dispatch(&query); err != nil { + if err == bus.ErrHandlerNotFound { + return nil + } + if err == m.ErrDataSourceAccessDenied { + return Error(403, err.Error(), nil) + } + return Error(500, "Failed to check data source permissions", err) + } + + return nil +} + +func GetDataSourceById(c *m.ReqContext) Response { query := m.GetDataSourceByIdQuery{ Id: c.ParamsInt64(":id"), OrgId: c.OrgId, @@ -68,14 +88,14 @@ func GetDataSourceByID(c *m.ReqContext) Response { return JSON(200, &dtos) } -func DeleteDataSourceByID(c *m.ReqContext) Response { +func DeleteDataSourceById(c *m.ReqContext) Response { id := c.ParamsInt64(":id") if id <= 0 { return Error(400, "Missing valid datasource id", nil) } - ds, err := getRawDataSourceByID(id, c.OrgId) + ds, err := getRawDataSourceById(id, c.OrgId) if err != nil { return Error(400, "Failed to delete datasource", nil) } @@ -186,7 +206,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error { return nil } - ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId) + ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId) if err != nil { return err } @@ -206,7 +226,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error { return nil } -func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) { +func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) { query := m.GetDataSourceByIdQuery{ Id: id, OrgId: orgID, @@ -236,7 +256,7 @@ func GetDataSourceByName(c *m.ReqContext) Response { } // Get /api/datasources/id/:name -func GetDataSourceIDByName(c *m.ReqContext) Response { +func GetDataSourceIdByName(c *m.ReqContext) Response { query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index b2f4a620208..8794d7d8338 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -32,7 +32,6 @@ import ( _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" - _ "github.com/grafana/grafana/pkg/services/datasources" _ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/rendering" diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index cbdd0136f4d..d602acb3ed2 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -29,6 +29,7 @@ var ( ErrDataSourceNameExists = errors.New("Data source with same name already exists") ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource") ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.") + ErrDataSourceAccessDenied = errors.New("Data source access denied") ) type DsAccess string @@ -165,6 +166,7 @@ type DeleteDataSourceByNameCommand struct { type GetDataSourcesQuery struct { OrgId int64 + User *SignedInUser Result []*DataSource } @@ -185,6 +187,26 @@ type GetDataSourceByNameQuery struct { } // --------------------- -// EVENTS -type DataSourceCreatedEvent struct { +// Permissions +// --------------------- + +type DataSourcePermissionType int + +const ( + DsPermissionQuery DataSourcePermissionType = 1 << iota + DsPermissionAdmin +) + +func (p DataSourcePermissionType) String() string { + names := map[int]string{ + int(DsPermissionQuery): "Query", + int(DsPermissionAdmin): "Admin", + } + return names[int(p)] +} + +type HasRequiredDataSourcePermissionQuery struct { + Id int64 + User *SignedInUser + RequiredPermission DataSourcePermissionType } diff --git a/pkg/services/datasources/datasource_service.go b/pkg/services/datasources/datasource_service.go deleted file mode 100644 index 2fba0bb5b87..00000000000 --- a/pkg/services/datasources/datasource_service.go +++ /dev/null @@ -1,50 +0,0 @@ -package datasources - -import ( - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/setting" -) - -type DataSourceService interface { - GetById(id int64, user *models.SignedInUser) (*models.DataSource, error) -} - -type DataSourceServiceImpl struct { - log log.Logger - Cfg *setting.Cfg `inject:""` - Guardian DataSourceGuardian `inject:""` -} - -func init() { - registry.RegisterService(&DataSourceServiceImpl{}) - registry.RegisterService(&DataSourceGuardianNoop{}) -} - -func (srv *DataSourceServiceImpl) Init() error { - srv.log = log.New("datasources") - srv.log.Info("hello", "guardian", srv.Guardian.GetPermission(0, nil)) - return nil -} - -func (srv *DataSourceServiceImpl) GetById(id int64, user *models.SignedInUser) { - // check cache - // Get by id from db - // check permissions -} - -type DataSourceGuardian interface { - GetPermission(id int64, user *models.SignedInUser) bool -} - -type DataSourceGuardianNoop struct { -} - -func (dsg *DataSourceGuardianNoop) Init() error { - return nil -} - -func (dsg *DataSourceGuardianNoop) GetPermission(id int64, user *models.SignedInUser) bool { - return false -} diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 00d520bcfc6..7f70e5c25fc 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -27,6 +27,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id} has, err := x.Get(&datasource) + if err != nil { return err } From 75f832cda8ef15e72c700a903ecc6cbb81349d79 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 1 Oct 2018 14:13:03 +0200 Subject: [PATCH 301/878] use alert state changes counter as secondary version --- pkg/models/alert.go | 4 ++-- pkg/models/alert_notifications.go | 3 ++- pkg/services/alerting/notifier.go | 5 ++-- pkg/services/alerting/result_handler.go | 5 ++++ pkg/services/alerting/rule.go | 2 ++ pkg/services/sqlstore/alert.go | 2 ++ pkg/services/sqlstore/alert_notification.go | 15 ++++++++---- .../sqlstore/alert_notification_test.go | 23 +++++++++++++++++++ pkg/services/sqlstore/migrations/alert_mig.go | 4 ++++ 9 files changed, 54 insertions(+), 9 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index fba2aa63df9..ba1fc0779ba 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -75,7 +75,7 @@ type Alert struct { EvalData *simplejson.Json NewStateDate time.Time - StateChanges int + StateChanges int64 Created time.Time Updated time.Time @@ -156,7 +156,7 @@ type SetAlertStateCommand struct { Error string EvalData *simplejson.Json - Timestamp time.Time + Result Alert } //Queries diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 8608303ddde..2d50185e33d 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -97,7 +97,8 @@ type AlertNotificationState struct { } type SetAlertNotificationStateToPendingCommand struct { - State *AlertNotificationState + AlertRuleStateUpdatedVersion int64 + State *AlertNotificationState } type SetAlertNotificationStateToCompleteCommand struct { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 4f69514977a..cc60e61fb4c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -94,7 +94,8 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *NotifierState) error { if !evalContext.IsTestRun { setPendingCmd := &m.SetAlertNotificationStateToPendingCommand{ - State: notifierState.state, + State: notifierState.state, + AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges, } err := bus.DispatchCtx(evalContext.Ctx, setPendingCmd) @@ -172,7 +173,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] for _, notification := range query.Result { not, err := n.createNotifierFor(notification) if err != nil { - n.log.Error("Could not create notifier", "notifier", notification.Id) + n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err) continue } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index e2c70de0e28..455296fbfc7 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -67,6 +67,11 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } handler.log.Error("Failed to save state", "error", err) + } else { + + // StateChanges is used for de dupping alert notifications + // when two servers are raising. + evalContext.Rule.StateChanges = cmd.Result.StateChanges } // save annotation diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 018d138dbe4..ce135ea31eb 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,6 +23,8 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 + + StateChanges int64 } type ValidationError struct { diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index b8206db191a..2f17402b80c 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -279,6 +279,8 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { } sess.ID(alert.Id).Update(&alert) + + cmd.Result = alert return nil }) } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 1fc0394414b..01a81023dff 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -277,19 +277,26 @@ func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAl sql := `UPDATE alert_notification_state SET state = ?, version = ?, - updated_at = ? + updated_at = ?, + alert_rule_state_updated_version = ? WHERE id = ? AND - version = ?` + (version = ? OR alert_rule_state_updated_version < ?)` - res, err := sess.Exec(sql, cmd.State.State, cmd.State.Version, timeNow().Unix(), cmd.State.Id, currentVersion) + res, err := sess.Exec(sql, + cmd.State.State, + cmd.State.Version, + timeNow().Unix(), + cmd.AlertRuleStateUpdatedVersion, + cmd.State.Id, + currentVersion, + cmd.AlertRuleStateUpdatedVersion) if err != nil { return err } affected, _ := res.RowsAffected() - if affected == 0 { return m.ErrAlertNotificationStateVersionConflict } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index f82022fd18b..9bcf2c18f4d 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -100,6 +100,29 @@ func TestAlertNotificationSQLAccess(t *testing.T) { err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) }) + + Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() { + s := *query.Result + cmd := models.SetAlertNotificationStateToPendingCommand{ + State: &s, + AlertRuleStateUpdatedVersion: 1000, + } + err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + So(err, ShouldBeNil) + + So(cmd.State.Version, ShouldEqual, 1) + So(cmd.State.State, ShouldEqual, models.AlertNotificationStatePending) + }) + + Convey("different version and same alert state change version should return error", func() { + s := *query.Result + s.Version = 1000 + cmd := models.SetAlertNotificationStateToPendingCommand{ + State: &s, + } + err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + So(err, ShouldNotBeNil) + }) }) Reset(func() { diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index bd42bb0343d..5b76f0273fd 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -130,4 +130,8 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state)) mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) + + mg.AddMigration("Add alert_rule_state_updated_version to alert_notification_state", NewAddColumnMigration(alert_notification_state, &Column{ + Name: "alert_rule_state_updated_version", Type: DB_BigInt, Nullable: true, + })) } From 043d5f1c05f409ee295b20a50e80ee096821bfa2 Mon Sep 17 00:00:00 2001 From: Steven Arnott Date: Mon, 1 Oct 2018 11:41:19 -0400 Subject: [PATCH 302/878] Update ldap.md --- docs/sources/auth/ldap.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index 82db8214fb7..4a884a60d15 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -181,6 +181,7 @@ 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]))" group_search_filter_user_attribute = "cn" ``` +For more information on AD searches see [Microsoft's Search Filter Syntax](https://docs.microsoft.com/en-us/windows/desktop/adsi/search-filter-syntax) documentation. For troubleshooting, by changing `member_of` in `[servers.attributes]` to "dn" it will show you more accurate group memberships when [debug is enabled](#troubleshooting). From 3c8820ab55bb43b21d4b938f82ad821486cf33ed Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 1 Oct 2018 18:01:26 +0200 Subject: [PATCH 303/878] invites table --- .../components/OrgActionBar/OrgActionBar.tsx | 11 +- public/app/features/users/InviteesTable.tsx | 59 ++++++++++ public/app/features/users/UsersActionBar.tsx | 80 ++++++++++++++ public/app/features/users/UsersListPage.tsx | 75 +++++++++---- public/app/features/users/UsersTable.tsx | 102 +++++++++--------- public/app/features/users/state/actions.ts | 29 ++++- public/app/features/users/state/reducers.ts | 16 ++- public/app/features/users/state/selectors.ts | 9 ++ public/app/types/index.ts | 3 +- public/app/types/users.ts | 22 ++++ 10 files changed, 318 insertions(+), 88 deletions(-) create mode 100644 public/app/features/users/InviteesTable.tsx create mode 100644 public/app/features/users/UsersActionBar.tsx diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index f3112ffaefd..de91c6cc6b3 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -4,19 +4,14 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; export interface Props { searchQuery: string; layoutMode?: LayoutMode; - showLayoutMode: boolean; setLayoutMode?: (mode: LayoutMode) => {}; setSearchQuery: (value: string) => {}; linkButton: { href: string; title: string }; } export default class OrgActionBar extends PureComponent { - static defaultProps = { - showLayoutMode: true, - }; - render() { - const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props; + const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props; return (
    @@ -31,9 +26,7 @@ export default class OrgActionBar extends PureComponent { /> - {showLayoutMode && ( - setLayoutMode(mode)} /> - )} + setLayoutMode(mode)} />
    diff --git a/public/app/features/users/InviteesTable.tsx b/public/app/features/users/InviteesTable.tsx new file mode 100644 index 00000000000..82c02e607de --- /dev/null +++ b/public/app/features/users/InviteesTable.tsx @@ -0,0 +1,59 @@ +import React, { createRef, PureComponent } from 'react'; +import { Invitee } from 'app/types'; + +export interface Props { + invitees: Invitee[]; + revokeInvite: (code: string) => void; +} + +export default class InviteesTable extends PureComponent { + private copyRef = createRef(); + + copyToClipboard = () => { + const node = this.copyRef.current; + + if (node) { + node.select(); + document.execCommand('copy'); + } + }; + + render() { + const { invitees, revokeInvite } = this.props; + + return ( + + + + + + + + + {invitees.map((invitee, index) => { + return ( + + + +
    EmailName + +
    {invitee.email}{invitee.name} + - - Back - +
    + This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource. +
    -
    -
    -
    +
    + + + Back +
    - +
    +
    +
    + + diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index c03c1befa12..17aedd48afd 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -71,15 +71,15 @@

    Auth

    - +
    - +
    - +
    diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index a70a1de98a4..4de2fadd52d 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -6,18 +6,18 @@
    - Database + Database
    - User + User
    - Password + Password
    From 8e2859625fb705eb8988c900e1999d82f2722095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 5 Oct 2018 13:13:04 -0700 Subject: [PATCH 401/878] ux: more minor ds setting tweaks --- .../app/plugins/datasource/stackdriver/partials/config.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index d8029abc39f..46b79d8bb0d 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -81,4 +81,6 @@
    -

    Do not forget to save your changes after uploading a file.

    +
    + Do not forget to save your changes after uploading a file. +
    From 69cf131f81110abc8f09f0ecbce8e582fe8e331f Mon Sep 17 00:00:00 2001 From: Emil Hessman Date: Sat, 6 Oct 2018 17:09:41 +0200 Subject: [PATCH 402/878] docs: fix minor typos --- PLUGIN_DEV.md | 4 ++-- docs/sources/administration/permissions.md | 4 ++-- docs/sources/administration/provisioning.md | 2 +- docs/sources/auth/overview.md | 2 +- docs/sources/contribute/cla.md | 2 +- docs/sources/features/datasources/mssql.md | 2 +- docs/sources/features/datasources/mysql.md | 2 +- docs/sources/features/datasources/opentsdb.md | 2 +- docs/sources/features/panels/alertlist.md | 2 +- docs/sources/features/panels/heatmap.md | 2 +- docs/sources/guides/whats-new-in-v2-5.md | 2 +- docs/sources/guides/whats-new-in-v2.md | 2 +- docs/sources/guides/whats-new-in-v3-1.md | 4 ++-- docs/sources/guides/whats-new-in-v3.md | 2 +- docs/sources/guides/whats-new-in-v4-2.md | 2 +- docs/sources/guides/whats-new-in-v4-5.md | 4 ++-- docs/sources/guides/whats-new-in-v4-6.md | 2 +- docs/sources/http_api/alerting.md | 2 +- docs/sources/http_api/dashboard_versions.md | 2 +- docs/sources/tutorials/ha_setup.md | 4 ++-- 20 files changed, 25 insertions(+), 25 deletions(-) diff --git a/PLUGIN_DEV.md b/PLUGIN_DEV.md index 4e2e080ebe6..168b21dbd88 100644 --- a/PLUGIN_DEV.md +++ b/PLUGIN_DEV.md @@ -6,8 +6,8 @@ upgrading Grafana please check here before creating an issue. ## Links -- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource) -- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource) +- [Datasource plugin written in TypeScript](https://github.com/grafana/typescript-template-datasource) +- [Simple JSON datasource plugin](https://github.com/grafana/simple-json-datasource) - [Plugin development guide](http://docs.grafana.org/plugins/developing/development/) - [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack) diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md index 1d1a70607c8..0d374f03647 100644 --- a/docs/sources/administration/permissions.md +++ b/docs/sources/administration/permissions.md @@ -55,7 +55,7 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se {{< 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 -remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. +remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. @@ -102,7 +102,7 @@ Permissions for a dashboard: Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. -- **View**: Can only view existing dashboars/folders. +- **View**: Can only view existing dashboards/folders. - You cannot override permissions for users with **Org Admin Role** - A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 16d425d289a..336ef9bfc3e 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -217,7 +217,7 @@ Note: The JSON shown in input field and when using `Copy JSON to Clipboard` and/ {{< docs-imagebox img="/img/docs/v51/provisioning_cannot_save_dashboard.png" max-width="500px" class="docs-image--no-shadow" >}} -### Reuseable Dashboard Urls +### Reusable Dashboard Urls If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifier. When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated. diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 20010a9ac09..a372600ac46 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -58,7 +58,7 @@ If you change your organization name in the Grafana UI this setting needs to be ### Basic authentication Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP -authenticaten integration. +authentication integration. To disable basic auth: diff --git a/docs/sources/contribute/cla.md b/docs/sources/contribute/cla.md index ffb2aaef1b9..a073a9a4eae 100644 --- a/docs/sources/contribute/cla.md +++ b/docs/sources/contribute/cla.md @@ -101,4 +101,4 @@ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US]


    -This CLA agreement is based on the [Harmony Contributor Aggrement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/) +This CLA agreement is based on the [Harmony Contributor Agreement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index debf771ffb0..a8399804344 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -225,7 +225,7 @@ When above query are used in a graph panel the result will be two series named ` {{< docs-imagebox img="/img/docs/v51/mssql_time_series_two.png" class="docs-image--no-shadow docs-image--right" >}} -**Example with multiple `value` culumns:** +**Example with multiple `value` columns:** ```sql SELECT diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index d713a4b42b7..590f4dec65e 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -59,7 +59,7 @@ Identifier | Description The database user you specify when you add the data source should only be granted SELECT permissions on the specified database & tables you want to query. Grafana does not validate that the query is safe. The query could include any SQL statement. For example, statements like `USE otherdb;` and `DROP TABLE user;` would be -executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions. +executed. To protect against this we **Highly** recommend you create a specific mysql user with restricted permissions. Example: diff --git a/docs/sources/features/datasources/opentsdb.md b/docs/sources/features/datasources/opentsdb.md index 1f6f022a18c..d2cd0b1dc0e 100644 --- a/docs/sources/features/datasources/opentsdb.md +++ b/docs/sources/features/datasources/opentsdb.md @@ -84,7 +84,7 @@ Some examples are mentioned below to make nested template queries work successfu Query | Description ------------ | ------------- *tag_values(cpu, hostname, env=$env)* | Return tag values for cpu metric, selected env tag value and tag key hostname -*tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname +*tag_values(cpu, hostname, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) diff --git a/docs/sources/features/panels/alertlist.md b/docs/sources/features/panels/alertlist.md index 58aa2c0966a..a1ea8f0f600 100644 --- a/docs/sources/features/panels/alertlist.md +++ b/docs/sources/features/panels/alertlist.md @@ -22,6 +22,6 @@ The alert list panel allows you to display your dashboards alerts. The list can 1. **Show**: Lets you choose between current state or recent state changes. 2. **Max Items**: Max items set the maximum of items in a list. -3. **Sort Order**: Lets you sort your list alphabeticaly(asc/desc) or by importance. +3. **Sort Order**: Lets you sort your list alphabetically(asc/desc) or by importance. 4. **Alerts From** This Dashboard`: Shows alerts only from the dashboard the alert list is in. 5. **State Filter**: Here you can filter your list by one or more parameters. diff --git a/docs/sources/features/panels/heatmap.md b/docs/sources/features/panels/heatmap.md index 56ffe29f20f..aa87fbef1df 100644 --- a/docs/sources/features/panels/heatmap.md +++ b/docs/sources/features/panels/heatmap.md @@ -80,7 +80,7 @@ the upper or lower bound of the interval. There are a number of datasources supporting histogram over time like Elasticsearch (by using a Histogram bucket aggregation) or Prometheus (with [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric type and *Format as* option set to Heatmap). But generally, any datasource could be used if it meets the requirements: -returns series with names representing bucket bound or returns sereis sorted by the bound in ascending order. +returns series with names representing bucket bound or returns series sorted by the bound in ascending order. With Elasticsearch you control the size of the buckets using the Histogram interval (Y-Axis) and the Date Histogram interval (X-axis). diff --git a/docs/sources/guides/whats-new-in-v2-5.md b/docs/sources/guides/whats-new-in-v2-5.md index 90270ea1121..08d51ba5bd7 100644 --- a/docs/sources/guides/whats-new-in-v2-5.md +++ b/docs/sources/guides/whats-new-in-v2-5.md @@ -25,7 +25,7 @@ correctly in UTC mode.
    This release brings a fully featured query editor for Elasticsearch. You will now be able to visualize -logs or any kind of data stored in Elasticserarch. The query editor allows you to build both simple +logs or any kind of data stored in Elasticsearch. The query editor allows you to build both simple and complex queries for logs or metrics. - Compute metrics from your documents, supported Elasticsearch aggregations: diff --git a/docs/sources/guides/whats-new-in-v2.md b/docs/sources/guides/whats-new-in-v2.md index 499849c8d83..28d068b1cd6 100644 --- a/docs/sources/guides/whats-new-in-v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -34,7 +34,7 @@ Organizations via a role. That role can be: There are currently no permissions on individual dashboards. -Read more about Grafanas new user model on the [Admin section](../reference/admin/) +Read more about Grafana's new user model on the [Admin section](../reference/admin/) ## Dashboard Snapshot sharing diff --git a/docs/sources/guides/whats-new-in-v3-1.md b/docs/sources/guides/whats-new-in-v3-1.md index 1e8ef87297b..ab6c5281275 100644 --- a/docs/sources/guides/whats-new-in-v3-1.md +++ b/docs/sources/guides/whats-new-in-v3-1.md @@ -21,7 +21,7 @@ The export feature is now accessed from the share menu. Dashboards exported from Grafana 3.1 are now more portable and easier for others to import than before. The export process extracts information data source types used by panels and adds these to a new `inputs` section in the dashboard json. So when you or another person tries to import the dashboard they will be asked to -select data source and optional metrix prefix options. +select data source and optional metric prefix options. @@ -53,7 +53,7 @@ Grafana url to share with a colleague without having to use the Share modal. ## Internal metrics -Do you want metrics about viewing metrics? Ofc you do! In this release we added support for sending metrics about Grafana to graphite. +Do you want metrics about viewing metrics? Of course you do! In this release we added support for sending metrics about Grafana to graphite. You can configure interval and server in the config file. ## Logging diff --git a/docs/sources/guides/whats-new-in-v3.md b/docs/sources/guides/whats-new-in-v3.md index d82a833ec90..dbd9b685a2b 100644 --- a/docs/sources/guides/whats-new-in-v3.md +++ b/docs/sources/guides/whats-new-in-v3.md @@ -197,7 +197,7 @@ you can install it manually from [Grafana.com](https://grafana.com) ## Plugin showcase Discovering and installing plugins is very quick and easy with Grafana 3.0 and [Grafana.com](https://grafana.com). Here -are a couple that I incurage you try! +are a couple that I encourage you try! #### [Clock Panel](https://grafana.com/plugins/grafana-clock-panel) Support's both current time and count down mode. diff --git a/docs/sources/guides/whats-new-in-v4-2.md b/docs/sources/guides/whats-new-in-v4-2.md index e36e762bb76..7a00023172a 100644 --- a/docs/sources/guides/whats-new-in-v4-2.md +++ b/docs/sources/guides/whats-new-in-v4-2.md @@ -45,7 +45,7 @@ We might add more global built in variables in the future and if we do we will p ### Dedupe alert notifications when running multiple servers -In this release we will dedupe alert notificiations when you are running multiple servers. +In this release we will dedupe alert notifications when you are running multiple servers. This makes it possible to run alerting on multiple servers and only get one notification. We currently solve this with sql transactions which puts some limitations for how many servers you can use to execute the same rules. diff --git a/docs/sources/guides/whats-new-in-v4-5.md b/docs/sources/guides/whats-new-in-v4-5.md index a5cd3ca982d..c6cfcf64720 100644 --- a/docs/sources/guides/whats-new-in-v4-5.md +++ b/docs/sources/guides/whats-new-in-v4-5.md @@ -45,7 +45,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins ### Enhancements * **GitHub OAuth**: Support for GitHub organizations with 100+ teams. [#8846](https://github.com/grafana/grafana/issues/8846), thx [@skwashd](https://github.com/skwashd) -* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboad time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055) +* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboard time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055) * **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261) - **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095). @@ -53,7 +53,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins * **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit. -This option is now rennamed (and moved to Options sub section above your queries): +This option is now renamed (and moved to Options sub section above your queries): ![image|519x120](upload://ySjHOVpavV6yk9LHQxL9nq2HIsT.png) Datas source selection & options & help are now above your metric queries. diff --git a/docs/sources/guides/whats-new-in-v4-6.md b/docs/sources/guides/whats-new-in-v4-6.md index ee0c4ea7a04..91fa74084a8 100644 --- a/docs/sources/guides/whats-new-in-v4-6.md +++ b/docs/sources/guides/whats-new-in-v4-6.md @@ -61,7 +61,7 @@ This makes exploring and filtering Prometheus data much easier. ### Minor Changes * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) -* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) +* **Dataproxy**: Allow Grafana to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367) * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) * **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 032fd508dd0..103de190793 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -227,7 +227,7 @@ Content-Type: application/json ## Create alert notification -You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. +You can find the full list of [supported notifiers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. `POST /api/alert-notifications` diff --git a/docs/sources/http_api/dashboard_versions.md b/docs/sources/http_api/dashboard_versions.md index 3d0ec27a3a3..0be22674997 100644 --- a/docs/sources/http_api/dashboard_versions.md +++ b/docs/sources/http_api/dashboard_versions.md @@ -291,7 +291,7 @@ Content-Type: text/html; charset=UTF-8

    ``` -The response is a textual respresentation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab. +The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab. Status Codes: diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 5fdb091a348..f141392e223 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -26,9 +26,9 @@ Grafana will now persist all long term data in the database. How to configure th ## User sessions -The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana. +The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana. Grafana supports two ways of storing session data: locally on disk or in a database/cache-server. -If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server +If you want to store sessions on disk you can use `sticky sessions` in your load balancer. If you prefer to store session data in a database/cache-server you can use any stateless routing strategy in your load balancer (ex round robin or least connections). ### Sticky sessions From 4815f92f6fabd9d155b8e07e462657ec6186baa4 Mon Sep 17 00:00:00 2001 From: Jordan Neufeld Date: Sat, 6 Oct 2018 10:14:14 -0500 Subject: [PATCH 403/878] Fix text overflow on playlist search #13464 --- public/sass/pages/_playlist.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index 5dd1c92cbd2..b8802940818 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -84,11 +84,11 @@ background-color: $list-item-bg; margin-bottom: 4px; .search-result-icon:before { - content: "\f009"; + content: '\f009'; } &.search-item-dash-home .search-result-icon:before { - content: "\f015"; + content: '\f015'; } } @@ -105,7 +105,10 @@ .playlist-available-list { td { line-height: 2rem; + max-width: 335px; white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } .add-dashboard { From 2e4a1f317d78822443db38287a80ab6e3a7daa16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 6 Oct 2018 19:22:16 +0200 Subject: [PATCH 404/878] ux: final fixes to new datasource page --- .../app/features/datasources/state/actions.ts | 23 ++++++++++--------- public/sass/_variables.dark.scss | 2 +- public/sass/components/_add_data_source.scss | 3 ++- public/sass/components/_cards.scss | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index f78a8f30e74..33d6b79c5df 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -82,21 +82,22 @@ export function loadDataSources(): ThunkResult { export function addDataSource(plugin: Plugin): ThunkResult { return async (dispatch, getStore) => { - let dataSources = getStore().dataSources.dataSources; + await dispatch(loadDataSources()); - if (dataSources.length === 0) { - dispatch(loadDataSources()); + const dataSources = getStore().dataSources.dataSources; - dataSources = getStore().dataSources.dataSources; + const newInstance = { + name: plugin.name, + type: plugin.id, + access: 'proxy', + isDefault: dataSources.length === 0, + }; + + if (nameExits(dataSources, newInstance.name)) { + newInstance.name = findNewName(dataSources, newInstance.name); } - let name = plugin.name; - - if (nameExits(dataSources, name)) { - name = findNewName(dataSources, name); - } - - const result = await getBackendSrv().post('/api/datasources', { name: name, type: plugin.id, access: 'proxy' }); + const result = await getBackendSrv().post('/api/datasources', newInstance); dispatch(updateLocation({ path: `/datasources/edit/${result.id}` })); }; } diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 8a012ea0a32..a878db0d9f4 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -120,7 +120,7 @@ $code-tag-border: lighten($code-tag-bg, 2%); // cards $card-background: linear-gradient(135deg, #2f2f32, #262628); -$card-background-hover: linear-gradient(135deg, $dark-3, $dark-2); +$card-background-hover: linear-gradient(135deg, #343436, #262628); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3); // Lists diff --git a/public/sass/components/_add_data_source.scss b/public/sass/components/_add_data_source.scss index d46974fe97e..508f7f80d8e 100644 --- a/public/sass/components/_add_data_source.scss +++ b/public/sass/components/_add_data_source.scss @@ -28,10 +28,11 @@ cursor: pointer; background: $card-background; box-shadow: $card-shadow; - color: $headings-color; + color: $text-color; &:hover { background: $card-background-hover; + color: $text-color-strong; } } diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 11a8abb7640..f39be84ec04 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -191,6 +191,7 @@ .card-item-wrapper { padding: 0; width: 100%; + margin-bottom: 3px; } .card-item-wrapper--clickable { @@ -198,7 +199,6 @@ } .card-item { - border-bottom: 3px solid $page-bg; border-radius: 2px; } From 2f84101fe768eb2865b2bea16f943636c71beb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 7 Oct 2018 10:39:47 -0700 Subject: [PATCH 405/878] wip: restoring old angular panel tabs / edit mode --- .../app/features/panel/metrics_panel_ctrl.ts | 4 +++- public/app/features/panel/panel_ctrl.ts | 23 ++----------------- public/app/features/panel/panel_directive.ts | 4 +++- public/app/plugins/panel/graph/module.ts | 2 +- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index f7a3e22a134..67848e460fb 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -7,6 +7,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { getExploreUrl } from 'app/core/utils/explore'; +import { metricsTabDirective } from './metrics_tab'; class MetricsPanelCtrl extends PanelCtrl { scope: any; @@ -56,7 +57,8 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - // this.addCommonTab('Time range', 'public/app/features/panel/partials/panelTime.html'); + this.addEditorTab('Queries', metricsTabDirective, 1, 'fa fa-database'); + this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); } private onMetricsPanelRefresh() { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index fbef69bc42a..51dd38e4358 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -6,8 +6,6 @@ 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; @@ -22,7 +20,6 @@ export class PanelCtrl { pluginName: string; pluginId: string; editorTabs: any; - optionTabs: any; $scope: any; $injector: any; $location: any; @@ -97,15 +94,11 @@ export class PanelCtrl { initEditMode() { this.editorTabs = []; - this.optionTabs = []; - this.addCommonTab('Queries', metricsTabDirective, 0, 'fa fa-database'); - this.addCommonTab('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); - // this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); - const urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase(); if (urlTab) { this.editorTabs.forEach((tab, i) => { @@ -123,7 +116,7 @@ export class PanelCtrl { route.updateParams(); } - addCommonTab(title, directiveFn, index?, icon?) { + addEditorTab(title, directiveFn, index?, icon?) { const editorTab = { title, directiveFn, icon }; if (_.isString(directiveFn)) { @@ -139,18 +132,6 @@ 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_directive.ts b/public/app/features/panel/panel_directive.ts index 9ce4edd3f52..def7c69a69d 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -32,11 +32,13 @@ const panelTemplate = ` 'panel-height-helper': !ctrl.panel.isEditing}">
    +

    + {{ctrl.pluginName}} +

    • - {{::tab.title}}
    • diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index d9d67f11b6c..584b58ae3ce 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -139,7 +139,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); if (config.alertingEnabled) { - this.addCommonTab('Alert', alertTab, 5); + this.addEditorTab('Alert', alertTab, 5); } this.subTabIndex = 0; From 67f5bb2c4eade449668ea7c014dd4a71b435d83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 8 Oct 2018 09:19:48 +0200 Subject: [PATCH 406/878] fix for influxdb annotation issue that caused text to be shown twice, fixes #13553 --- public/app/plugins/datasource/influxdb/influx_series.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/influx_series.ts b/public/app/plugins/datasource/influxdb/influx_series.ts index d2a8482eced..10c1584f488 100644 --- a/public/app/plugins/datasource/influxdb/influx_series.ts +++ b/public/app/plugins/datasource/influxdb/influx_series.ts @@ -99,9 +99,6 @@ export default class InfluxSeries { if (column === 'sequence_number') { return; } - if (!titleCol) { - titleCol = index; - } if (column === this.annotation.titleColumn) { titleCol = index; return; @@ -114,6 +111,10 @@ export default class InfluxSeries { textCol = index; return; } + // legacy case + if (!titleCol && textCol !== index) { + titleCol = index; + } }); _.each(series.values, value => { From 4ecd33c79ceab85ad2b91b3ab13ba6dac59bee74 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 8 Oct 2018 14:09:02 +0200 Subject: [PATCH 407/878] Fixed nav model --- .../datasources/EditDataSourcePage.tsx | 90 +++++++++++++++++ .../app/features/datasources/state/actions.ts | 27 +++++- .../features/datasources/state/navModel.ts | 97 +++++++++++++++++++ .../features/datasources/state/reducers.ts | 4 + .../features/datasources/state/selectors.ts | 9 ++ public/app/routes/routes.ts | 15 ++- public/app/types/datasources.ts | 1 + 7 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 public/app/features/datasources/EditDataSourcePage.tsx create mode 100644 public/app/features/datasources/state/navModel.ts diff --git a/public/app/features/datasources/EditDataSourcePage.tsx b/public/app/features/datasources/EditDataSourcePage.tsx new file mode 100644 index 00000000000..7c19266d53a --- /dev/null +++ b/public/app/features/datasources/EditDataSourcePage.tsx @@ -0,0 +1,90 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from '../../core/components/PageHeader/PageHeader'; +import { DataSource, NavModel } from 'app/types'; +import { loadDataSource } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; +import { getDataSourceLoadingNav } from './state/navModel'; +import { getDataSource } from './state/selectors'; + +export interface Props { + navModel: NavModel; + dataSource: DataSource; + dataSourceId: number; + pageName: string; + loadDataSource: typeof loadDataSource; +} + +enum PageTypes { + Settings = 'settings', + Permissions = 'permissions', + Dashboards = 'dashboards', +} + +export class EditDataSourcePage extends PureComponent { + componentDidMount() { + this.fetchDataSource(); + } + + async fetchDataSource() { + await this.props.loadDataSource(this.props.dataSourceId); + } + + isValidPage(currentPage) { + return (Object as any).values(PageTypes).includes(currentPage); + } + + getCurrentPage() { + const currentPage = this.props.pageName; + + return this.isValidPage(currentPage) ? currentPage : PageTypes.Settings; + } + + renderPage() { + switch (this.getCurrentPage()) { + case PageTypes.Settings: + return
      Settings
      ; + + case PageTypes.Permissions: + return
      Permissions
      ; + + case PageTypes.Dashboards: + return
      Dashboards
      ; + } + + return null; + } + + render() { + const { navModel } = this.props; + + return ( +
      + +
      + {this.renderPage()} +
      + ); + } +} + +function mapStateToProps(state) { + const pageName = getRouteParamsPage(state.location) || 'settings'; + const dataSourceId = getRouteParamsId(state.location); + const dataSourceLoadingNav = getDataSourceLoadingNav(pageName); + + return { + navModel: getNavModel(state.navIndex, `datasource-${pageName}-${dataSourceId}`, dataSourceLoadingNav), + dataSourceId: dataSourceId, + dataSource: getDataSource(state.dataSources, dataSourceId), + pageName: pageName, + }; +} + +const mapDispatchToProps = { + loadDataSource, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(EditDataSourcePage)); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 33d6b79c5df..2b6b986774f 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -2,12 +2,14 @@ import { ThunkAction } from 'redux-thunk'; import { DataSource, Plugin, StoreState } from 'app/types'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; -import { updateLocation } from '../../../core/actions'; +import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions'; import { UpdateLocationAction } from '../../../core/actions/location'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadDataSources = 'LOAD_DATA_SOURCES', LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES', + LoadDataSource = 'LOAD_DATA_SOURCE', SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY', @@ -38,11 +40,21 @@ export interface SetDataSourceTypeSearchQueryAction { payload: string; } +export interface LoadDataSourceAction { + type: ActionTypes.LoadDataSource; + payload: DataSource; +} + const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({ type: ActionTypes.LoadDataSources, payload: dataSources, }); +const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({ + type: ActionTypes.LoadDataSource, + payload: dataSource, +}); + const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({ type: ActionTypes.LoadDataSourceTypes, payload: dataSourceTypes, @@ -69,7 +81,9 @@ export type Action = | SetDataSourcesLayoutModeAction | UpdateLocationAction | LoadDataSourceTypesAction - | SetDataSourceTypeSearchQueryAction; + | SetDataSourceTypeSearchQueryAction + | LoadDataSourceAction + | UpdateNavIndexAction; type ThunkResult = ThunkAction; @@ -80,6 +94,15 @@ export function loadDataSources(): ThunkResult { }; } +export function loadDataSource(id: number): ThunkResult { + return async dispatch => { + const dataSource = await getBackendSrv().get(`/api/datasources/${id}`); + const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`); + dispatch(dataSourceLoaded(dataSource)); + dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo))); + }; +} + export function addDataSource(plugin: Plugin): ThunkResult { return async (dispatch, getStore) => { await dispatch(loadDataSources()); diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts new file mode 100644 index 00000000000..d80ab5d52a2 --- /dev/null +++ b/public/app/features/datasources/state/navModel.ts @@ -0,0 +1,97 @@ +import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types'; + +export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem { + const navModel = { + img: pluginMeta.info.logos.large, + id: 'datasource-' + dataSource.id, + subTitle: `Type: ${pluginMeta.name}`, + url: '', + text: dataSource.name, + breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `datasource-settings-${dataSource.id}`, + text: 'Settings', + url: `datasources/edit/${dataSource.id}/settings`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `datasource-permissions-${dataSource.id}`, + text: 'Permissions', + url: `datasources/edit/${dataSource.id}/permissions`, + }, + ], + }; + + if (pluginMeta.includes && pluginMeta.includes.length > 0) { + navModel.children.push({ + active: false, + icon: 'gicon gicon-dashboard', + id: `datasource-dashboards-${dataSource.id}`, + text: 'Dashboards', + url: `datasources/edit/${dataSource.id}/dashboards`, + }); + } + + return navModel; +} + +export function getDataSourceLoadingNav(pageName: string): NavModel { + const main = buildNavModel( + { + access: '', + basicAuth: false, + database: '', + id: 1, + isDefault: false, + jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' }, + name: 'Loading', + orgId: 1, + password: '', + readOnly: false, + type: 'Loading', + typeLogoUrl: 'public/img/icn-datasource.svg', + url: '', + user: '', + }, + { + id: '1', + name: '', + info: { + author: { + name: '', + url: '', + }, + description: '', + links: [''], + logos: { + large: '', + small: '', + }, + screenshots: '', + updated: '', + version: '', + }, + includes: [{ type: '', name: '', path: '' }], + } + ); + + 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/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index acb228d3ed6..075483945a4 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -4,6 +4,7 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec const initialState: DataSourcesState = { dataSources: [] as DataSource[], + dataSource: {} as DataSource, layoutMode: LayoutModes.Grid, searchQuery: '', dataSourcesCount: 0, @@ -16,6 +17,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo case ActionTypes.LoadDataSources: return { ...state, dataSources: action.payload, dataSourcesCount: action.payload.length }; + case ActionTypes.LoadDataSource: + return { ...state, dataSource: action.payload }; + case ActionTypes.SetDataSourcesSearchQuery: return { ...state, searchQuery: action.payload }; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 80e1400114f..eef176eb49a 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -1,3 +1,5 @@ +import { DataSource } from '../../../types'; + export const getDataSources = state => { const regex = new RegExp(state.searchQuery, 'i'); @@ -14,6 +16,13 @@ export const getDataSourceTypes = state => { }); }; +export const getDataSource = (state, dataSourceId): DataSource | null => { + if (state.dataSource.id === parseInt(dataSourceId, 10)) { + return state.dataSource; + } + return null; +}; + export const getDataSourcesSearchQuery = state => state.searchQuery; export const getDataSourcesLayoutMode = state => state.layoutMode; export const getDataSourcesCount = state => state.dataSourcesCount; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 9aecf53e7bb..54f4a6718e1 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -12,6 +12,7 @@ import FolderPermissions from 'app/features/folders/FolderPermissions'; import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; import NewDataSourcePage from '../features/datasources/NewDataSourcePage'; import UsersListPage from 'app/features/users/UsersListPage'; +import EditDataSourcePage from 'app/features/datasources/EditDataSourcePage'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -71,15 +72,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { component: () => DataSourcesListPage, }, }) - .when('/datasources/edit/:id', { - templateUrl: 'public/app/features/plugins/partials/ds_edit.html', - controller: 'DataSourceEditCtrl', - controllerAs: 'ctrl', - }) - .when('/datasources/edit/:id/dashboards', { - templateUrl: 'public/app/features/plugins/partials/ds_dashboards.html', - controller: 'DataSourceDashboardsCtrl', - controllerAs: 'ctrl', + .when('/datasources/edit/:id/:page?', { + template: '', + resolve: { + component: () => EditDataSourcePage, + }, }) .when('/datasources/new', { template: '', diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 4d8d755f106..9d35794b89e 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -25,4 +25,5 @@ export interface DataSourcesState { layoutMode: LayoutMode; dataSourcesCount: number; dataSourceTypes: Plugin[]; + dataSource: DataSource; } From 6fce178ec7a94d1b63a0a08fc57e2c45b11b70e2 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 8 Oct 2018 15:34:28 +0200 Subject: [PATCH 408/878] stackdriver metric name fix. Fixes #13562 Sets metric name even when the metric does not have a displayName field. Closes #13562. --- .../plugins/datasource/stackdriver/datasource.ts | 12 +++++++++++- .../datasource/stackdriver/query_filter_ctrl.ts | 12 ++++-------- .../datasource/stackdriver/specs/datasource.test.ts | 13 +++++++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 8ff81f3160a..7ea748e1082 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -241,7 +241,17 @@ export default class StackdriverDatasource { try { const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`; const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); - return data.metricDescriptors; + + const metrics = data.metricDescriptors.map(m => { + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); + m.service = service; + m.serviceShortName = serviceShortName; + m.displayName = m.displayName || m.type; + return m; + }); + + return metrics; } catch (error) { console.log(error); } diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index ac279eec0d5..786b2831e89 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -96,11 +96,9 @@ export class StackdriverFilterCtrl { getServicesList() { const defaultValue = { value: this.$scope.defaultServiceValue, text: this.$scope.defaultServiceValue }; const services = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); return { - value: service, - text: serviceShortName, + value: m.service, + text: m.serviceShortName, }; }); @@ -113,12 +111,10 @@ export class StackdriverFilterCtrl { getMetricsList() { const metrics = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); return { - service, + service: m.service, value: m.type, - serviceShortName, + serviceShortName: m.serviceShortName, text: m.displayName, title: m.description, }; diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 80830fd4d68..3117be402a9 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -164,11 +164,11 @@ describe('StackdriverDataSource', () => { metricDescriptors: [ { displayName: 'test metric name 1', - type: 'test metric type 1', + type: 'compute.googleapis.com/instance/cpu/test-metric-type-1', + description: 'A description', }, { - displayName: 'test metric name 2', - type: 'test metric type 2', + type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name', }, ], }, @@ -180,8 +180,13 @@ describe('StackdriverDataSource', () => { }); it('should return successfully', () => { expect(result.length).toBe(2); - expect(result[0].type).toBe('test metric type 1'); + expect(result[0].service).toBe('compute.googleapis.com'); + expect(result[0].serviceShortName).toBe('compute'); + expect(result[0].type).toBe('compute.googleapis.com/instance/cpu/test-metric-type-1'); expect(result[0].displayName).toBe('test metric name 1'); + expect(result[0].description).toBe('A description'); + expect(result[1].type).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); + expect(result[1].displayName).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); }); }); From 4d8f594d31ea3fa6d82f6fb249abf16059f8decd Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:07:46 +0200 Subject: [PATCH 409/878] stackdriver: interpolate stackdriver filter wildcards when asterix is used in filter --- pkg/tsdb/stackdriver/stackdriver.go | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 586e154cd5d..e802a85fc24 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -15,6 +15,8 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/api/pluginproxy" @@ -159,6 +161,53 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd return stackdriverQueries, nil } +func reverse(s string) string { + chars := []rune(s) + for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 { + chars[i], chars[j] = chars[j], chars[i] + } + return string(chars) +} + +func escapeDoubleBackslash(target string) string { + var re = regexp.MustCompile(`\\`) + return re.ReplaceAllString(target, `\\\\`) + // return strings.Replace(target, `\`, "", -1) +} + +func escapeIllegalCharacters(target string) string { + var re = regexp.MustCompile(`[-\/^$+?.()|[\]{}]`) + return string(re.ReplaceAllFunc([]byte(target), func(in []byte) []byte { + return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1)) + })) +} + +func replaceSingleAsterixCharacters(target string) string { + return strings.Replace(target, "*", ".*", -1) +} + +func interpolateFilterWildcards(value string) string { + if strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { + value = strings.Replace(value, "*", "", 1) + value = fmt.Sprintf(`has_substring("%s")`, value) + } else if strings.HasPrefix(value, "*") { + value = strings.Replace(value, "*", "", 1) + value = fmt.Sprintf(`ends_with("%s")`, value) + } else if strings.HasSuffix(value, "*") { + value = reverse(strings.Replace(reverse(value), "*", "", 1)) + value = fmt.Sprintf(`starts_with("%s")`, value) + } else if strings.Contains(value, "*") { + value = escapeIllegalCharacters(value) + value = replaceSingleAsterixCharacters(value) + value = strings.Replace(value, `"`, `\\"`, -1) + value = fmt.Sprintf(`monitoring.regex.full_match("^%s$")`, value) + } + + logger.Info("filter", "filter", value) + + return value +} + func buildFilterString(metricType string, filterParts []interface{}) string { filterString := "" for i, part := range filterParts { @@ -166,7 +215,11 @@ func buildFilterString(metricType string, filterParts []interface{}) string { if part == "AND" { filterString += " " } else if mod == 2 { - filterString += fmt.Sprintf(`"%s"`, part) + if strings.Contains(part.(string), "*") { + filterString += interpolateFilterWildcards(part.(string)) + } else { + filterString += fmt.Sprintf(`"%s"`, part) + } } else { filterString += part.(string) } @@ -231,6 +284,7 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv } req.URL.RawQuery = query.Params.Encode() + logger.Info("req.URL.RawQuery", "req.URL.RawQuery", req.URL.RawQuery) queryResult.Meta.Set("rawQuery", req.URL.RawQuery) alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"] From 2e665fba0f6c8a9b83f58e10922a9538d1ede966 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:11:05 +0200 Subject: [PATCH 410/878] stackdriver: remove not necessary helper functions --- pkg/tsdb/stackdriver/stackdriver.go | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index e802a85fc24..962b238de4c 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -169,23 +169,6 @@ func reverse(s string) string { return string(chars) } -func escapeDoubleBackslash(target string) string { - var re = regexp.MustCompile(`\\`) - return re.ReplaceAllString(target, `\\\\`) - // return strings.Replace(target, `\`, "", -1) -} - -func escapeIllegalCharacters(target string) string { - var re = regexp.MustCompile(`[-\/^$+?.()|[\]{}]`) - return string(re.ReplaceAllFunc([]byte(target), func(in []byte) []byte { - return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1)) - })) -} - -func replaceSingleAsterixCharacters(target string) string { - return strings.Replace(target, "*", ".*", -1) -} - func interpolateFilterWildcards(value string) string { if strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", 1) @@ -197,8 +180,11 @@ func interpolateFilterWildcards(value string) string { value = reverse(strings.Replace(reverse(value), "*", "", 1)) value = fmt.Sprintf(`starts_with("%s")`, value) } else if strings.Contains(value, "*") { - value = escapeIllegalCharacters(value) - value = replaceSingleAsterixCharacters(value) + re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`) + value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte { + return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1)) + })) + value = strings.Replace(value, "*", ".*", -1) value = strings.Replace(value, `"`, `\\"`, -1) value = fmt.Sprintf(`monitoring.regex.full_match("^%s$")`, value) } From 68332c595171a1ba83dc9193411d2ac0d3c69490 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:29:51 +0200 Subject: [PATCH 411/878] stackdriver: fix broken substring. also adds tests --- pkg/tsdb/stackdriver/stackdriver.go | 7 +++++-- pkg/tsdb/stackdriver/stackdriver_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 962b238de4c..ec698a77ce0 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -170,8 +170,11 @@ func reverse(s string) string { } func interpolateFilterWildcards(value string) string { - if strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { - value = strings.Replace(value, "*", "", 1) + re := regexp.MustCompile("[*]") + matches := re.FindAllStringIndex(value, -1) + logger.Info("len", "len", len(matches)) + if len(matches) == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { + value = strings.Replace(value, "*", "", -1) value = fmt.Sprintf(`has_substring("%s")`, value) } else if strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", 1) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index da4d6890207..59bda5a98b4 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -342,6 +342,19 @@ func TestStackdriver(t *testing.T) { }) }) }) + + Convey("when interpolating filter wildcards", func() { + Convey("and wildcard is used in the beginning and the end of the word", func() { + Convey("and theres no wildcard in the middle of the word", func() { + value := interpolateFilterWildcards("*-central1*") + So(value, ShouldEqual, `has_substring("-central1")`) + }) + Convey("and there is a wildcard in the middle of the word", func() { + value := interpolateFilterWildcards("*-cent*ral1*") + So(value, ShouldNotStartWith, `has_substring`) + }) + }) + }) }) } From 035be6cbbe5354aa4f0c2b0db2f09b228e2effe7 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:52:26 +0200 Subject: [PATCH 412/878] stackdriver: add more tests --- pkg/tsdb/stackdriver/stackdriver.go | 12 +++---- pkg/tsdb/stackdriver/stackdriver_test.go | 44 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index ec698a77ce0..0f09de61644 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -171,18 +171,18 @@ func reverse(s string) string { func interpolateFilterWildcards(value string) string { re := regexp.MustCompile("[*]") - matches := re.FindAllStringIndex(value, -1) - logger.Info("len", "len", len(matches)) - if len(matches) == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { + matches := len(re.FindAllStringIndex(value, -1)) + logger.Info("len", "len", matches) + if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", -1) value = fmt.Sprintf(`has_substring("%s")`, value) - } else if strings.HasPrefix(value, "*") { + } else if matches == 1 && strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", 1) value = fmt.Sprintf(`ends_with("%s")`, value) - } else if strings.HasSuffix(value, "*") { + } else if matches == 1 && strings.HasSuffix(value, "*") { value = reverse(strings.Replace(reverse(value), "*", "", 1)) value = fmt.Sprintf(`starts_with("%s")`, value) - } else if strings.Contains(value, "*") { + } else if matches == 1 { re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`) value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte { return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1)) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 59bda5a98b4..5184c6fc3bb 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -354,6 +354,50 @@ func TestStackdriver(t *testing.T) { So(value, ShouldNotStartWith, `has_substring`) }) }) + + Convey("and wildcard is used in the beginning of the word", func() { + Convey("and there is not a wildcard elsewhere in the word", func() { + value := interpolateFilterWildcards("*-central1") + So(value, ShouldEqual, `ends_with("-central1")`) + }) + Convey("and there is a wildcard elsewhere in the word", func() { + value := interpolateFilterWildcards("*-cent*al1") + So(value, ShouldNotStartWith, `ends_with`) + }) + }) + + Convey("and wildcard is used at the end of the word", func() { + Convey("and there is not a wildcard elsewhere in the word", func() { + value := interpolateFilterWildcards("us-central*") + So(value, ShouldEqual, `starts_with("us-central")`) + }) + Convey("and there is a wildcard elsewhere in the word", func() { + value := interpolateFilterWildcards("*us-central*") + So(value, ShouldNotStartWith, `starts_with`) + }) + }) + + Convey("and wildcard is used in the middle of the word", func() { + Convey("and there is only one wildcard", func() { + value := interpolateFilterWildcards("us-ce*tral1-b") + So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-b$")`) + }) + + Convey("and there is more than one wildcard", func() { + value := interpolateFilterWildcards("us-ce*tra*1-b") + So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tra.*1\\-b$")`) + }) + }) + + Convey("and wildcard is used in the middle of the word and in the beginning of the word", func() { + value := interpolateFilterWildcards("*s-ce*tral1-b") + So(value, ShouldEqual, `monitoring.regex.full_match("^.*s\\-ce.*tral1\\-b$")`) + }) + + Convey("and wildcard is used in the middle of the word and in the ending of the word", func() { + value := interpolateFilterWildcards("us-ce*tral1-*") + So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-.*$")`) + }) }) }) } From 2a0d7a88039224627acee3291b70dbc5b1bd814c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:53:19 +0200 Subject: [PATCH 413/878] stackdriver: remove debug logging --- pkg/tsdb/stackdriver/stackdriver.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 0f09de61644..ce7ca8fdee4 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -15,8 +15,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/api/pluginproxy" @@ -172,7 +170,6 @@ func reverse(s string) string { func interpolateFilterWildcards(value string) string { re := regexp.MustCompile("[*]") matches := len(re.FindAllStringIndex(value, -1)) - logger.Info("len", "len", matches) if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", -1) value = fmt.Sprintf(`has_substring("%s")`, value) @@ -192,8 +189,6 @@ func interpolateFilterWildcards(value string) string { value = fmt.Sprintf(`monitoring.regex.full_match("^%s$")`, value) } - logger.Info("filter", "filter", value) - return value } @@ -273,7 +268,6 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv } req.URL.RawQuery = query.Params.Encode() - logger.Info("req.URL.RawQuery", "req.URL.RawQuery", req.URL.RawQuery) queryResult.Meta.Set("rawQuery", req.URL.RawQuery) alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"] From 5f7795aa1f525e34f5aba659175827887ede3a91 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 17:58:31 +0200 Subject: [PATCH 414/878] stackdriver: test that no interpolation is done when there are no wildcards --- pkg/tsdb/stackdriver/stackdriver.go | 2 +- pkg/tsdb/stackdriver/stackdriver_test.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index ce7ca8fdee4..8b5d71ba830 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -179,7 +179,7 @@ func interpolateFilterWildcards(value string) string { } else if matches == 1 && strings.HasSuffix(value, "*") { value = reverse(strings.Replace(reverse(value), "*", "", 1)) value = fmt.Sprintf(`starts_with("%s")`, value) - } else if matches == 1 { + } else if matches != 0 { re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`) value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte { return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1)) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 5184c6fc3bb..5840514b993 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -398,7 +398,13 @@ func TestStackdriver(t *testing.T) { value := interpolateFilterWildcards("us-ce*tral1-*") So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-.*$")`) }) + + Convey("and no wildcard is used", func() { + value := interpolateFilterWildcards("us-central1-a}") + So(value, ShouldEqual, `us-central1-a}`) + }) }) + }) } From a3122a4b854672f210892f6f158f7a074dd1d8f5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 2 Oct 2018 18:09:42 +0200 Subject: [PATCH 415/878] stackdriver: test build filter string --- pkg/tsdb/stackdriver/stackdriver_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 5840514b993..ec311c9f50f 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -405,6 +405,19 @@ func TestStackdriver(t *testing.T) { }) }) + Convey("when building filter string", func() { + Convey("and there are wildcards in a filter value", func() { + filterParts := []interface{}{"zone", "=", "*-central1*"} + value := buildFilterString("somemetrictype", filterParts) + So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`) + }) + + Convey("and there are no wildcards in any filter value", func() { + filterParts := []interface{}{"zone", "=", "us-central1-a"} + value := buildFilterString("somemetrictype", filterParts) + So(value, ShouldEqual, `metric.type="somemetrictype" zone="us-central1-a"`) + }) + }) }) } From 46ca306c2f742223d3f6aa546f4805c8d30cb31f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 10:52:18 +0200 Subject: [PATCH 416/878] stackdriver: always use regex full match for =~ and !=~operator --- .../features/datasources/stackdriver.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 96f3ba3382e..6c493829e50 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -74,8 +74,12 @@ Click on the links above and click the `Enable` button: Choose a metric from the `Metric` dropdown. +### Filter + To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1` +It is also possible to add wildcards to the filter value field. E.g `us-*` to capture all values that starts with "us-", `*central-a` to capture all that ends with "central-a". `*-central-*` captures values that has the substring of -central-. + ### Aggregation The aggregation field lets you combine time series based on common statistics. Read more about this option [here](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options). @@ -105,20 +109,20 @@ The Alias By field allows you to control the format of the legend keys. The defa #### Metric Type Patterns -Alias Pattern | Description | Example Result ------------------ | ---------------------------- | ------------- -`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` -`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` -`{{metric.service}}` | returns the service part | `compute` +| Alias Pattern | Description | Example Result | +| -------------------- | ---------------------------- | ------------------------------------------------- | +| `{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` | +| `{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` | +| `{{metric.service}}` | returns the service part | `compute` | #### Label Patterns In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns. -Alias Pattern Format | Description | Alias Pattern Example | Example Result ----------------------- | ---------------------------------- | ---------------------------- | ------------- -`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` -`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` +| Alias Pattern Format | Description | Alias Pattern Example | Example Result | +| ------------------------ | -------------------------------- | -------------------------------- | ---------------- | +| `{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` | +| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` | Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}` From 7e6a5c0a7436e175383dbbe93e9f869d15c4ccbb Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 11:08:14 +0200 Subject: [PATCH 417/878] stackdriver: add tests from regex matching --- pkg/tsdb/stackdriver/stackdriver_test.go | 29 ++++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index ec311c9f50f..8b1e8308ef7 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -406,16 +406,31 @@ func TestStackdriver(t *testing.T) { }) Convey("when building filter string", func() { - Convey("and there are wildcards in a filter value", func() { - filterParts := []interface{}{"zone", "=", "*-central1*"} - value := buildFilterString("somemetrictype", filterParts) - So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`) + Convey("and theres no regex operator", func() { + Convey("and there are wildcards in a filter value", func() { + filterParts := []interface{}{"zone", "=", "*-central1*"} + value := buildFilterString("somemetrictype", filterParts) + So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`) + }) + + Convey("and there are no wildcards in any filter value", func() { + filterParts := []interface{}{"zone", "!=", "us-central1-a"} + value := buildFilterString("somemetrictype", filterParts) + So(value, ShouldEqual, `metric.type="somemetrictype" zone!="us-central1-a"`) + }) }) - Convey("and there are no wildcards in any filter value", func() { - filterParts := []interface{}{"zone", "=", "us-central1-a"} + Convey("and there is a regex operator", func() { + filterParts := []interface{}{"zone", "=~", "us-central1-a~"} value := buildFilterString("somemetrictype", filterParts) - So(value, ShouldEqual, `metric.type="somemetrictype" zone="us-central1-a"`) + Convey("it should remove the ~ character from the operator that belongs to the value", func() { + So(value, ShouldNotContainSubstring, `=~`) + So(value, ShouldContainSubstring, `zone=`) + }) + + Convey("it should insert monitoring.regex.full_match before filter value", func() { + So(value, ShouldContainSubstring, `zone=monitoring.regex.full_match("us-central1-a~")`) + }) }) }) }) From 8d53799bcdd2f7ce434ef20e389df44464271828 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 11:12:26 +0200 Subject: [PATCH 418/878] stackdriver: always use regex full match for =~ and !=~operator --- pkg/tsdb/stackdriver/stackdriver.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 8b5d71ba830..9023ef7735e 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -199,7 +199,11 @@ func buildFilterString(metricType string, filterParts []interface{}) string { if part == "AND" { filterString += " " } else if mod == 2 { - if strings.Contains(part.(string), "*") { + operator := filterParts[i-1] + if operator == "=~" || operator == "!=~" { + filterString = reverse(strings.Replace(reverse(filterString), "~", "", 1)) + filterString += fmt.Sprintf(`monitoring.regex.full_match("%s")`, part) + } else if strings.Contains(part.(string), "*") { filterString += interpolateFilterWildcards(part.(string)) } else { filterString += fmt.Sprintf(`"%s"`, part) From 11b9f9691cb181f7b3322ef24cd85b2d616e73dc Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 12:01:11 +0200 Subject: [PATCH 419/878] stackdriver: improve filter docs for wildcards and regular expressions --- docs/sources/features/datasources/stackdriver.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 6c493829e50..c525130aebb 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -76,9 +76,15 @@ Choose a metric from the `Metric` dropdown. ### Filter -To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1` +To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`. -It is also possible to add wildcards to the filter value field. E.g `us-*` to capture all values that starts with "us-", `*central-a` to capture all that ends with "central-a". `*-central-*` captures values that has the substring of -central-. +#### Simple wildcards + +When the operator is set to `=` or `!=` it is possible to add wildcards to the filter value field. E.g `us-*` will capture all values that starts with "us-" and `*central-a` will capture all values that ends with "central-a". `*-central-*` captures all values that has the substring of -central-. Simple wildcards are less expensive than regular expressions. + +#### Regular expressions + +When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions. ### Aggregation From 61cac5fd617bf65245fa48a023c3dc63bb7a7b78 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 8 Oct 2018 16:01:17 +0200 Subject: [PATCH 420/878] reverted back and using angular for settings and dashboards --- .../datasources/DataSourceSettings.tsx | 125 ++++++++++++++++++ .../datasources/EditDataSourcePage.tsx | 13 +- .../app/features/datasources/state/actions.ts | 15 ++- .../features/datasources/state/navModel.ts | 30 +++-- .../features/datasources/state/reducers.ts | 4 + public/app/features/plugins/state/navModel.ts | 11 ++ public/app/routes/routes.ts | 10 ++ public/app/types/datasources.ts | 7 +- 8 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 public/app/features/datasources/DataSourceSettings.tsx diff --git a/public/app/features/datasources/DataSourceSettings.tsx b/public/app/features/datasources/DataSourceSettings.tsx new file mode 100644 index 00000000000..f7d641d34b0 --- /dev/null +++ b/public/app/features/datasources/DataSourceSettings.tsx @@ -0,0 +1,125 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { DataSource, Plugin } from 'app/types'; + +export interface Props { + dataSource: DataSource; + dataSourceMeta: Plugin; +} +interface State { + name: string; +} + +enum DataSourceStates { + Alpha = 'alpha', + Beta = 'beta', +} + +export class DataSourceSettings extends PureComponent { + constructor(props) { + super(props); + + this.state = { + name: props.dataSource.name, + }; + } + + onNameChange = event => { + this.setState({ + name: event.target.value, + }); + }; + + onSubmit = event => { + event.preventDefault(); + console.log(event); + }; + + onDelete = event => { + console.log(event); + }; + + isReadyOnly() { + return this.props.dataSource.readOnly === true; + } + + shouldRenderInfoBox() { + const { state } = this.props.dataSourceMeta; + + return state === DataSourceStates.Alpha || state === DataSourceStates.Beta; + } + + getInfoText() { + const { dataSourceMeta } = this.props; + + switch (dataSourceMeta.state) { + case DataSourceStates.Alpha: + return ( + 'This plugin is marked as being in alpha state, which means it is in early development phase and updates' + + ' will include breaking changes.' + ); + + case DataSourceStates.Beta: + return ( + 'This plugin is marked as being in a beta development state. This means it is in currently in active' + + ' development and could be missing important features.' + ); + } + + return null; + } + + render() { + const { name } = this.state; + + return ( +
      +

      Settings

      +
      +
      +
      +
      + Name + +
      +
      +
      + {this.shouldRenderInfoBox() &&
      {this.getInfoText()}
      } + {this.isReadyOnly() && ( +
      + This datasource was added by config and cannot be modified using the UI. Please contact your server admin + to update this datasource. +
      + )} +
      + + + + Back + +
      +
      +
      + ); + } +} + +function mapStateToProps(state) { + return { + dataSource: state.dataSources.dataSource, + dataSourceMeta: state.dataSources.dataSourceMeta, + }; +} + +export default connect(mapStateToProps)(DataSourceSettings); diff --git a/public/app/features/datasources/EditDataSourcePage.tsx b/public/app/features/datasources/EditDataSourcePage.tsx index 7c19266d53a..46966dcbb58 100644 --- a/public/app/features/datasources/EditDataSourcePage.tsx +++ b/public/app/features/datasources/EditDataSourcePage.tsx @@ -39,19 +39,13 @@ export class EditDataSourcePage extends PureComponent { getCurrentPage() { const currentPage = this.props.pageName; - return this.isValidPage(currentPage) ? currentPage : PageTypes.Settings; + return this.isValidPage(currentPage) ? currentPage : PageTypes.Permissions; } renderPage() { switch (this.getCurrentPage()) { - case PageTypes.Settings: - return
      Settings
      ; - case PageTypes.Permissions: return
      Permissions
      ; - - case PageTypes.Dashboards: - return
      Dashboards
      ; } return null; @@ -63,15 +57,14 @@ export class EditDataSourcePage extends PureComponent { return (
      -
      - {this.renderPage()} +
      {this.renderPage()}
      ); } } function mapStateToProps(state) { - const pageName = getRouteParamsPage(state.location) || 'settings'; + const pageName = getRouteParamsPage(state.location) || PageTypes.Permissions; const dataSourceId = getRouteParamsId(state.location); const dataSourceLoadingNav = getDataSourceLoadingNav(pageName); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 2b6b986774f..bb8fce8424a 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -10,6 +10,7 @@ export enum ActionTypes { LoadDataSources = 'LOAD_DATA_SOURCES', LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES', LoadDataSource = 'LOAD_DATA_SOURCE', + LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META', SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY', @@ -45,6 +46,11 @@ export interface LoadDataSourceAction { payload: DataSource; } +export interface LoadDataSourceMetaAction { + type: ActionTypes.LoadDataSourceMeta; + payload: Plugin; +} + const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({ type: ActionTypes.LoadDataSources, payload: dataSources, @@ -55,6 +61,11 @@ const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({ payload: dataSource, }); +const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({ + type: ActionTypes.LoadDataSourceMeta, + payload: dataSourceMeta, +}); + const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({ type: ActionTypes.LoadDataSourceTypes, payload: dataSourceTypes, @@ -83,7 +94,8 @@ export type Action = | LoadDataSourceTypesAction | SetDataSourceTypeSearchQueryAction | LoadDataSourceAction - | UpdateNavIndexAction; + | UpdateNavIndexAction + | LoadDataSourceMetaAction; type ThunkResult = ThunkAction; @@ -99,6 +111,7 @@ export function loadDataSource(id: number): ThunkResult { const dataSource = await getBackendSrv().get(`/api/datasources/${id}`); const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`); dispatch(dataSourceLoaded(dataSource)); + dispatch(dataSourceMetaLoaded(pluginInfo)); dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo))); }; } diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index d80ab5d52a2..47eadb82376 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -1,4 +1,5 @@ import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types'; +import config from 'app/core/config'; export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem { const navModel = { @@ -16,26 +17,29 @@ export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): N text: 'Settings', url: `datasources/edit/${dataSource.id}/settings`, }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `datasource-permissions-${dataSource.id}`, - text: 'Permissions', - url: `datasources/edit/${dataSource.id}/permissions`, - }, ], }; - if (pluginMeta.includes && pluginMeta.includes.length > 0) { + if (pluginMeta.includes && hasDashboards(pluginMeta.includes)) { navModel.children.push({ active: false, - icon: 'gicon gicon-dashboard', + icon: 'fa fa-fw fa-th-large', id: `datasource-dashboards-${dataSource.id}`, text: 'Dashboards', url: `datasources/edit/${dataSource.id}/dashboards`, }); } + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-lock', + id: `datasource-permissions-${dataSource.id}`, + text: 'Permissions', + url: `datasources/edit/${dataSource.id}/permissions`, + }); + } + return navModel; } @@ -95,3 +99,11 @@ export function getDataSourceLoadingNav(pageName: string): NavModel { node: node, }; } + +function hasDashboards(includes) { + return ( + includes.filter(include => { + return include.type === 'dashboard'; + }).length > 0 + ); +} diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 075483945a4..051966c81be 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -10,6 +10,7 @@ const initialState: DataSourcesState = { dataSourcesCount: 0, dataSourceTypes: [] as Plugin[], dataSourceTypeSearchQuery: '', + dataSourceMeta: {} as Plugin, }; export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { @@ -31,6 +32,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo case ActionTypes.SetDataSourceTypeSearchQuery: return { ...state, dataSourceTypeSearchQuery: action.payload }; + + case ActionTypes.LoadDataSourceMeta: + return { ...state, dataSourceMeta: action.payload }; } return state; diff --git a/public/app/features/plugins/state/navModel.ts b/public/app/features/plugins/state/navModel.ts index 852eb2806f9..f12967ebb7a 100644 --- a/public/app/features/plugins/state/navModel.ts +++ b/public/app/features/plugins/state/navModel.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import { DataSource, PluginMeta, NavModel } from 'app/types'; +import config from 'app/core/config'; export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel { let title = 'New'; @@ -38,6 +39,16 @@ export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: s }); } + if (config.buildInfo.isEnterprise) { + main.children.push({ + active: currentPage === 'datasource-permissions', + icon: 'fa fa-fw fa-lock', + id: 'datasource-permissions', + text: 'Permissions', + url: `datasources/edit/${ds.id}/permissions`, + }); + } + return { main: main, node: _.find(main.children, { active: true }), diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 54f4a6718e1..43c513aea38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -72,6 +72,16 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { component: () => DataSourcesListPage, }, }) + .when('/datasources/edit/:id', { + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', + controller: 'DataSourceEditCtrl', + controllerAs: 'ctrl', + }) + .when('/datasources/edit/:id/dashboards', { + templateUrl: 'public/app/features/plugins/partials/ds_dashboards.html', + controller: 'DataSourceDashboardsCtrl', + controllerAs: 'ctrl', + }) .when('/datasources/edit/:id/:page?', { template: '', resolve: { diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 9d35794b89e..95c754faa6b 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -12,10 +12,10 @@ export interface DataSource { password: string; user: string; database: string; - basicAuth: false; - isDefault: false; + basicAuth: boolean; + isDefault: boolean; jsonData: { authType: string; defaultRegion: string }; - readOnly: false; + readOnly: boolean; } export interface DataSourcesState { @@ -26,4 +26,5 @@ export interface DataSourcesState { dataSourcesCount: number; dataSourceTypes: Plugin[]; dataSource: DataSource; + dataSourceMeta: Plugin; } From b283845e4e9854a1bbe552d7b8fd0a1bf07d557e Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 8 Oct 2018 16:05:37 +0200 Subject: [PATCH 421/878] adding permissions component --- .../datasources/DataSourcePermissions.tsx | 20 +++++++++++++++++++ .../datasources/EditDataSourcePage.tsx | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 public/app/features/datasources/DataSourcePermissions.tsx diff --git a/public/app/features/datasources/DataSourcePermissions.tsx b/public/app/features/datasources/DataSourcePermissions.tsx new file mode 100644 index 00000000000..7dfa71a2652 --- /dev/null +++ b/public/app/features/datasources/DataSourcePermissions.tsx @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; + +export interface Props {} + +export class DataSourcePermissions extends PureComponent { + render() { + return ( +
      +

      Permissions

      +
      + ); + } +} + +function mapStateToProps(state) { + return {}; +} + +export default connect(mapStateToProps)(DataSourcePermissions); diff --git a/public/app/features/datasources/EditDataSourcePage.tsx b/public/app/features/datasources/EditDataSourcePage.tsx index 46966dcbb58..1c7e67de0b9 100644 --- a/public/app/features/datasources/EditDataSourcePage.tsx +++ b/public/app/features/datasources/EditDataSourcePage.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from '../../core/components/PageHeader/PageHeader'; +import DataSourcePermissions from './DataSourcePermissions'; import { DataSource, NavModel } from 'app/types'; import { loadDataSource } from './state/actions'; import { getNavModel } from '../../core/selectors/navModel'; @@ -45,7 +46,7 @@ export class EditDataSourcePage extends PureComponent { renderPage() { switch (this.getCurrentPage()) { case PageTypes.Permissions: - return
      Permissions
      ; + return ; } return null; From d20b15834fe91bcd72be0464e1dc50fac268f480 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 28 Sep 2018 18:01:37 -0400 Subject: [PATCH 422/878] add gopkg.in/square/go-jose.v2 to dependencies, update github.com/hashicorp/yamux --- Gopkg.lock | 16 +- Gopkg.toml | 4 + pkg/extensions/main.go | 4 + vendor/github.com/hashicorp/yamux/mux.go | 13 +- vendor/github.com/hashicorp/yamux/session.go | 13 +- vendor/golang.org/x/crypto/ed25519/ed25519.go | 188 ++ .../ed25519/internal/edwards25519/const.go | 1422 +++++++++++++ .../internal/edwards25519/edwards25519.go | 1793 +++++++++++++++++ vendor/gopkg.in/square/go-jose.v2/LICENSE | 202 ++ .../gopkg.in/square/go-jose.v2/asymmetric.go | 592 ++++++ .../square/go-jose.v2/cipher/cbc_hmac.go | 196 ++ .../square/go-jose.v2/cipher/concat_kdf.go | 75 + .../square/go-jose.v2/cipher/ecdh_es.go | 62 + .../square/go-jose.v2/cipher/key_wrap.go | 109 + vendor/gopkg.in/square/go-jose.v2/crypter.go | 532 +++++ vendor/gopkg.in/square/go-jose.v2/doc.go | 27 + vendor/gopkg.in/square/go-jose.v2/encoding.go | 179 ++ .../gopkg.in/square/go-jose.v2/json/LICENSE | 27 + .../gopkg.in/square/go-jose.v2/json/decode.go | 1183 +++++++++++ .../gopkg.in/square/go-jose.v2/json/encode.go | 1197 +++++++++++ .../gopkg.in/square/go-jose.v2/json/indent.go | 141 ++ .../square/go-jose.v2/json/scanner.go | 623 ++++++ .../gopkg.in/square/go-jose.v2/json/stream.go | 480 +++++ .../gopkg.in/square/go-jose.v2/json/tags.go | 44 + vendor/gopkg.in/square/go-jose.v2/jwe.go | 294 +++ vendor/gopkg.in/square/go-jose.v2/jwk.go | 566 ++++++ vendor/gopkg.in/square/go-jose.v2/jws.go | 321 +++ vendor/gopkg.in/square/go-jose.v2/opaque.go | 83 + vendor/gopkg.in/square/go-jose.v2/shared.go | 494 +++++ vendor/gopkg.in/square/go-jose.v2/signing.go | 389 ++++ .../gopkg.in/square/go-jose.v2/symmetric.go | 482 +++++ 31 files changed, 11745 insertions(+), 6 deletions(-) create mode 100644 vendor/golang.org/x/crypto/ed25519/ed25519.go create mode 100644 vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go create mode 100644 vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/LICENSE create mode 100644 vendor/gopkg.in/square/go-jose.v2/asymmetric.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/crypter.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/doc.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/encoding.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/LICENSE create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/decode.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/encode.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/indent.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/scanner.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/stream.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/tags.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jwe.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jwk.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jws.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/opaque.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/shared.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/signing.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/symmetric.go diff --git a/Gopkg.lock b/Gopkg.lock index 041f784f770..4286add847d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -264,7 +264,7 @@ branch = "master" name = "github.com/hashicorp/yamux" packages = ["."] - revision = "2658be15c5f05e76244154714161f17e3e77de2e" + revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558" [[projects]] name = "github.com/inconshreveable/log15" @@ -507,6 +507,8 @@ branch = "master" name = "golang.org/x/crypto" packages = [ + "ed25519", + "ed25519/internal/edwards25519", "md4", "pbkdf2" ] @@ -670,6 +672,16 @@ revision = "e6179049628164864e6e84e973cfb56335748dea" version = "v2.3.2" +[[projects]] + name = "gopkg.in/square/go-jose.v2" + packages = [ + ".", + "cipher", + "json" + ] + revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" + version = "v2.1.9" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -679,6 +691,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6e9458f912a5f0eb3430b968f1b4dbc4e3b7671b282cf4fe1573419a6d9ba0d4" + inputs-digest = "6f7f271afd27f78b7d8ebe27436fee72c9925fb82a978bdc57fde44e01f3ca51" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index c5b4b31cb32..e3cbdeabb5d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -207,3 +207,7 @@ ignored = [ [[constraint]] name = "github.com/VividCortex/mysqlerr" branch = "master" + +[[constraint]] + name = "gopkg.in/square/go-jose.v2" + version = "2.1.9" diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 6e3461da8a8..1d8bbce03f3 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -1,3 +1,7 @@ package extensions +import ( + _ "gopkg.in/square/go-jose.v2" +) + var IsEnterprise bool = false diff --git a/vendor/github.com/hashicorp/yamux/mux.go b/vendor/github.com/hashicorp/yamux/mux.go index 7abc7c744ce..18a078c8ad9 100644 --- a/vendor/github.com/hashicorp/yamux/mux.go +++ b/vendor/github.com/hashicorp/yamux/mux.go @@ -3,6 +3,7 @@ package yamux import ( "fmt" "io" + "log" "os" "time" ) @@ -30,8 +31,13 @@ type Config struct { // window size that we allow for a stream. MaxStreamWindowSize uint32 - // LogOutput is used to control the log destination + // LogOutput is used to control the log destination. Either Logger or + // LogOutput can be set, not both. LogOutput io.Writer + + // Logger is used to pass in the logger to be used. Either Logger or + // LogOutput can be set, not both. + Logger *log.Logger } // DefaultConfig is used to return a default configuration @@ -57,6 +63,11 @@ func VerifyConfig(config *Config) error { if config.MaxStreamWindowSize < initialStreamWindow { return fmt.Errorf("MaxStreamWindowSize must be larger than %d", initialStreamWindow) } + if config.LogOutput != nil && config.Logger != nil { + return fmt.Errorf("both Logger and LogOutput may not be set, select one") + } else if config.LogOutput == nil && config.Logger == nil { + return fmt.Errorf("one of Logger or LogOutput must be set, select one") + } return nil } diff --git a/vendor/github.com/hashicorp/yamux/session.go b/vendor/github.com/hashicorp/yamux/session.go index d8446fa65ee..a80ddec35ea 100644 --- a/vendor/github.com/hashicorp/yamux/session.go +++ b/vendor/github.com/hashicorp/yamux/session.go @@ -86,9 +86,14 @@ type sendReady struct { // newSession is used to construct a new session func newSession(config *Config, conn io.ReadWriteCloser, client bool) *Session { + logger := config.Logger + if logger == nil { + logger = log.New(config.LogOutput, "", log.LstdFlags) + } + s := &Session{ config: config, - logger: log.New(config.LogOutput, "", log.LstdFlags), + logger: logger, conn: conn, bufRead: bufio.NewReader(conn), pings: make(map[uint32]chan struct{}), @@ -309,8 +314,10 @@ func (s *Session) keepalive() { case <-time.After(s.config.KeepAliveInterval): _, err := s.Ping() if err != nil { - s.logger.Printf("[ERR] yamux: keepalive failed: %v", err) - s.exitErr(ErrKeepAliveTimeout) + if err != ErrSessionShutdown { + s.logger.Printf("[ERR] yamux: keepalive failed: %v", err) + s.exitErr(ErrKeepAliveTimeout) + } return } case <-s.shutdownCh: diff --git a/vendor/golang.org/x/crypto/ed25519/ed25519.go b/vendor/golang.org/x/crypto/ed25519/ed25519.go new file mode 100644 index 00000000000..a57771a1ed3 --- /dev/null +++ b/vendor/golang.org/x/crypto/ed25519/ed25519.go @@ -0,0 +1,188 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package ed25519 implements the Ed25519 signature algorithm. See +// https://ed25519.cr.yp.to/. +// +// These functions are also compatible with the “Ed25519” function defined in +// RFC 8032. +package ed25519 + +// This code is a port of the public domain, “ref10” implementation of ed25519 +// from SUPERCOP. + +import ( + "bytes" + "crypto" + cryptorand "crypto/rand" + "crypto/sha512" + "errors" + "io" + "strconv" + + "golang.org/x/crypto/ed25519/internal/edwards25519" +) + +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = 32 + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = 64 + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = 64 +) + +// PublicKey is the type of Ed25519 public keys. +type PublicKey []byte + +// PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer. +type PrivateKey []byte + +// Public returns the PublicKey corresponding to priv. +func (priv PrivateKey) Public() crypto.PublicKey { + publicKey := make([]byte, PublicKeySize) + copy(publicKey, priv[32:]) + return PublicKey(publicKey) +} + +// Sign signs the given message with priv. +// Ed25519 performs two passes over messages to be signed and therefore cannot +// handle pre-hashed messages. Thus opts.HashFunc() must return zero to +// indicate the message hasn't been hashed. This can be achieved by passing +// crypto.Hash(0) as the value for opts. +func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { + if opts.HashFunc() != crypto.Hash(0) { + return nil, errors.New("ed25519: cannot sign hashed message") + } + + return Sign(priv, message), nil +} + +// GenerateKey generates a public/private key pair using entropy from rand. +// If rand is nil, crypto/rand.Reader will be used. +func GenerateKey(rand io.Reader) (publicKey PublicKey, privateKey PrivateKey, err error) { + if rand == nil { + rand = cryptorand.Reader + } + + privateKey = make([]byte, PrivateKeySize) + publicKey = make([]byte, PublicKeySize) + _, err = io.ReadFull(rand, privateKey[:32]) + if err != nil { + return nil, nil, err + } + + digest := sha512.Sum512(privateKey[:32]) + digest[0] &= 248 + digest[31] &= 127 + digest[31] |= 64 + + var A edwards25519.ExtendedGroupElement + var hBytes [32]byte + copy(hBytes[:], digest[:]) + edwards25519.GeScalarMultBase(&A, &hBytes) + var publicKeyBytes [32]byte + A.ToBytes(&publicKeyBytes) + + copy(privateKey[32:], publicKeyBytes[:]) + copy(publicKey, publicKeyBytes[:]) + + return publicKey, privateKey, nil +} + +// Sign signs the message with privateKey and returns a signature. It will +// panic if len(privateKey) is not PrivateKeySize. +func Sign(privateKey PrivateKey, message []byte) []byte { + if l := len(privateKey); l != PrivateKeySize { + panic("ed25519: bad private key length: " + strconv.Itoa(l)) + } + + h := sha512.New() + h.Write(privateKey[:32]) + + var digest1, messageDigest, hramDigest [64]byte + var expandedSecretKey [32]byte + h.Sum(digest1[:0]) + copy(expandedSecretKey[:], digest1[:]) + expandedSecretKey[0] &= 248 + expandedSecretKey[31] &= 63 + expandedSecretKey[31] |= 64 + + h.Reset() + h.Write(digest1[32:]) + h.Write(message) + h.Sum(messageDigest[:0]) + + var messageDigestReduced [32]byte + edwards25519.ScReduce(&messageDigestReduced, &messageDigest) + var R edwards25519.ExtendedGroupElement + edwards25519.GeScalarMultBase(&R, &messageDigestReduced) + + var encodedR [32]byte + R.ToBytes(&encodedR) + + h.Reset() + h.Write(encodedR[:]) + h.Write(privateKey[32:]) + h.Write(message) + h.Sum(hramDigest[:0]) + var hramDigestReduced [32]byte + edwards25519.ScReduce(&hramDigestReduced, &hramDigest) + + var s [32]byte + edwards25519.ScMulAdd(&s, &hramDigestReduced, &expandedSecretKey, &messageDigestReduced) + + signature := make([]byte, SignatureSize) + copy(signature[:], encodedR[:]) + copy(signature[32:], s[:]) + + return signature +} + +// Verify reports whether sig is a valid signature of message by publicKey. It +// will panic if len(publicKey) is not PublicKeySize. +func Verify(publicKey PublicKey, message, sig []byte) bool { + if l := len(publicKey); l != PublicKeySize { + panic("ed25519: bad public key length: " + strconv.Itoa(l)) + } + + if len(sig) != SignatureSize || sig[63]&224 != 0 { + return false + } + + var A edwards25519.ExtendedGroupElement + var publicKeyBytes [32]byte + copy(publicKeyBytes[:], publicKey) + if !A.FromBytes(&publicKeyBytes) { + return false + } + edwards25519.FeNeg(&A.X, &A.X) + edwards25519.FeNeg(&A.T, &A.T) + + h := sha512.New() + h.Write(sig[:32]) + h.Write(publicKey[:]) + h.Write(message) + var digest [64]byte + h.Sum(digest[:0]) + + var hReduced [32]byte + edwards25519.ScReduce(&hReduced, &digest) + + var R edwards25519.ProjectiveGroupElement + var s [32]byte + copy(s[:], sig[32:]) + + // https://tools.ietf.org/html/rfc8032#section-5.1.7 requires that s be in + // the range [0, order) in order to prevent signature malleability. + if !edwards25519.ScMinimal(&s) { + return false + } + + edwards25519.GeDoubleScalarMultVartime(&R, &hReduced, &A, &s) + + var checkR [32]byte + R.ToBytes(&checkR) + return bytes.Equal(sig[:32], checkR[:]) +} diff --git a/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go b/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go new file mode 100644 index 00000000000..e39f086c1d8 --- /dev/null +++ b/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go @@ -0,0 +1,1422 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package edwards25519 + +// These values are from the public domain, “ref10” implementation of ed25519 +// from SUPERCOP. + +// d is a constant in the Edwards curve equation. +var d = FieldElement{ + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116, +} + +// d2 is 2*d. +var d2 = FieldElement{ + -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199, +} + +// SqrtM1 is the square-root of -1 in the field. +var SqrtM1 = FieldElement{ + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482, +} + +// A is a constant in the Montgomery-form of curve25519. +var A = FieldElement{ + 486662, 0, 0, 0, 0, 0, 0, 0, 0, 0, +} + +// bi contains precomputed multiples of the base-point. See the Ed25519 paper +// for a discussion about how these values are used. +var bi = [8]PreComputedGroupElement{ + { + FieldElement{25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605}, + FieldElement{-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378}, + FieldElement{-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546}, + }, + { + FieldElement{15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024}, + FieldElement{16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574}, + FieldElement{30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357}, + }, + { + FieldElement{10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380}, + FieldElement{4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306}, + FieldElement{19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942}, + }, + { + FieldElement{5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766}, + FieldElement{-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701}, + FieldElement{28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300}, + }, + { + FieldElement{-22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877}, + FieldElement{-6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951}, + FieldElement{4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784}, + }, + { + FieldElement{-25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436}, + FieldElement{25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918}, + FieldElement{23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877}, + }, + { + FieldElement{-33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800}, + FieldElement{-25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305}, + FieldElement{-13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300}, + }, + { + FieldElement{-3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876}, + FieldElement{-24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619}, + FieldElement{-3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683}, + }, +} + +// base contains precomputed multiples of the base-point. See the Ed25519 paper +// for a discussion about how these values are used. +var base = [32][8]PreComputedGroupElement{ + { + { + FieldElement{25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605}, + FieldElement{-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378}, + FieldElement{-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546}, + }, + { + FieldElement{-12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303}, + FieldElement{-21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081}, + FieldElement{26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697}, + }, + { + FieldElement{15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024}, + FieldElement{16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574}, + FieldElement{30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357}, + }, + { + FieldElement{-17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540}, + FieldElement{23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397}, + FieldElement{7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325}, + }, + { + FieldElement{10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380}, + FieldElement{4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306}, + FieldElement{19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942}, + }, + { + FieldElement{-15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777}, + FieldElement{-8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737}, + FieldElement{-18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652}, + }, + { + FieldElement{5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766}, + FieldElement{-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701}, + FieldElement{28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300}, + }, + { + FieldElement{14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726}, + FieldElement{-7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955}, + FieldElement{27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425}, + }, + }, + { + { + FieldElement{-13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171}, + FieldElement{27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510}, + FieldElement{17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660}, + }, + { + FieldElement{-10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639}, + FieldElement{29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963}, + FieldElement{5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950}, + }, + { + FieldElement{-27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568}, + FieldElement{12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335}, + FieldElement{25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628}, + }, + { + FieldElement{-26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007}, + FieldElement{-2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772}, + FieldElement{-22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653}, + }, + { + FieldElement{2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567}, + FieldElement{13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686}, + FieldElement{21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372}, + }, + { + FieldElement{-13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887}, + FieldElement{-23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954}, + FieldElement{-29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953}, + }, + { + FieldElement{24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833}, + FieldElement{-16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532}, + FieldElement{-22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876}, + }, + { + FieldElement{2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268}, + FieldElement{33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214}, + FieldElement{1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038}, + }, + }, + { + { + FieldElement{6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800}, + FieldElement{4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645}, + FieldElement{-4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664}, + }, + { + FieldElement{1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933}, + FieldElement{-25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182}, + FieldElement{-17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222}, + }, + { + FieldElement{-18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991}, + FieldElement{20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880}, + FieldElement{9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092}, + }, + { + FieldElement{-16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295}, + FieldElement{19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788}, + FieldElement{8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553}, + }, + { + FieldElement{-15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026}, + FieldElement{11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347}, + FieldElement{-18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033}, + }, + { + FieldElement{-23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395}, + FieldElement{-27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278}, + FieldElement{1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890}, + }, + { + FieldElement{32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995}, + FieldElement{-30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596}, + FieldElement{-11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891}, + }, + { + FieldElement{31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060}, + FieldElement{11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608}, + FieldElement{-20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606}, + }, + }, + { + { + FieldElement{7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389}, + FieldElement{-19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016}, + FieldElement{-11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341}, + }, + { + FieldElement{-22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505}, + FieldElement{14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553}, + FieldElement{-28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655}, + }, + { + FieldElement{15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220}, + FieldElement{12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631}, + FieldElement{-4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099}, + }, + { + FieldElement{26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556}, + FieldElement{14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749}, + FieldElement{236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930}, + }, + { + FieldElement{1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391}, + FieldElement{5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253}, + FieldElement{20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066}, + }, + { + FieldElement{24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958}, + FieldElement{-11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082}, + FieldElement{-28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383}, + }, + { + FieldElement{-30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521}, + FieldElement{-11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807}, + FieldElement{23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948}, + }, + { + FieldElement{9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134}, + FieldElement{-32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455}, + FieldElement{27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629}, + }, + }, + { + { + FieldElement{-8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069}, + FieldElement{-32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746}, + FieldElement{24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919}, + }, + { + FieldElement{11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837}, + FieldElement{8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906}, + FieldElement{-28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771}, + }, + { + FieldElement{-25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817}, + FieldElement{10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098}, + FieldElement{10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409}, + }, + { + FieldElement{-12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504}, + FieldElement{-26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727}, + FieldElement{28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420}, + }, + { + FieldElement{-32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003}, + FieldElement{-1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605}, + FieldElement{-30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384}, + }, + { + FieldElement{-26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701}, + FieldElement{-23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683}, + FieldElement{29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708}, + }, + { + FieldElement{-3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563}, + FieldElement{-19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260}, + FieldElement{-5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387}, + }, + { + FieldElement{-19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672}, + FieldElement{23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686}, + FieldElement{-24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665}, + }, + }, + { + { + FieldElement{11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182}, + FieldElement{-31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277}, + FieldElement{14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628}, + }, + { + FieldElement{-4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474}, + FieldElement{-26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539}, + FieldElement{-25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822}, + }, + { + FieldElement{-10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970}, + FieldElement{19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756}, + FieldElement{-24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508}, + }, + { + FieldElement{-26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683}, + FieldElement{-10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655}, + FieldElement{-20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158}, + }, + { + FieldElement{-4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125}, + FieldElement{-15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839}, + FieldElement{-20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664}, + }, + { + FieldElement{27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294}, + FieldElement{-18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899}, + FieldElement{-11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070}, + }, + { + FieldElement{3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294}, + FieldElement{-15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949}, + FieldElement{-21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083}, + }, + { + FieldElement{31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420}, + FieldElement{-5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940}, + FieldElement{29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396}, + }, + }, + { + { + FieldElement{-12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567}, + FieldElement{20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127}, + FieldElement{-16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294}, + }, + { + FieldElement{-12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887}, + FieldElement{22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964}, + FieldElement{16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195}, + }, + { + FieldElement{9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244}, + FieldElement{24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999}, + FieldElement{-1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762}, + }, + { + FieldElement{-18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274}, + FieldElement{-33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236}, + FieldElement{-16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605}, + }, + { + FieldElement{-13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761}, + FieldElement{-22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884}, + FieldElement{-6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482}, + }, + { + FieldElement{-24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638}, + FieldElement{-11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490}, + FieldElement{-32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170}, + }, + { + FieldElement{5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736}, + FieldElement{10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124}, + FieldElement{-17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392}, + }, + { + FieldElement{8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029}, + FieldElement{6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048}, + FieldElement{28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958}, + }, + }, + { + { + FieldElement{24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593}, + FieldElement{26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071}, + FieldElement{-11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692}, + }, + { + FieldElement{11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687}, + FieldElement{-160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441}, + FieldElement{-20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001}, + }, + { + FieldElement{-938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460}, + FieldElement{-19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007}, + FieldElement{-21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762}, + }, + { + FieldElement{15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005}, + FieldElement{-9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674}, + FieldElement{4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035}, + }, + { + FieldElement{7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590}, + FieldElement{-2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957}, + FieldElement{-30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812}, + }, + { + FieldElement{33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740}, + FieldElement{-18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122}, + FieldElement{-27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158}, + }, + { + FieldElement{8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885}, + FieldElement{26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140}, + FieldElement{19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857}, + }, + { + FieldElement{801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155}, + FieldElement{19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260}, + FieldElement{19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483}, + }, + }, + { + { + FieldElement{-3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677}, + FieldElement{32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815}, + FieldElement{22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751}, + }, + { + FieldElement{-16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203}, + FieldElement{-11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208}, + FieldElement{1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230}, + }, + { + FieldElement{16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850}, + FieldElement{-21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389}, + FieldElement{-9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968}, + }, + { + FieldElement{-11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689}, + FieldElement{14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880}, + FieldElement{5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304}, + }, + { + FieldElement{30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632}, + FieldElement{-3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412}, + FieldElement{20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566}, + }, + { + FieldElement{-20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038}, + FieldElement{-26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232}, + FieldElement{-1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943}, + }, + { + FieldElement{17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856}, + FieldElement{23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738}, + FieldElement{15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971}, + }, + { + FieldElement{-27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718}, + FieldElement{-13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697}, + FieldElement{-11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883}, + }, + }, + { + { + FieldElement{5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912}, + FieldElement{-26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358}, + FieldElement{3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849}, + }, + { + FieldElement{29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307}, + FieldElement{-14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977}, + FieldElement{-6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335}, + }, + { + FieldElement{-29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644}, + FieldElement{-22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616}, + FieldElement{-27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735}, + }, + { + FieldElement{-21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099}, + FieldElement{29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341}, + FieldElement{-936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336}, + }, + { + FieldElement{-23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646}, + FieldElement{31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425}, + FieldElement{-17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388}, + }, + { + FieldElement{-31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743}, + FieldElement{-16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822}, + FieldElement{-8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462}, + }, + { + FieldElement{18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985}, + FieldElement{9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702}, + FieldElement{-22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797}, + }, + { + FieldElement{21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293}, + FieldElement{27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100}, + FieldElement{19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688}, + }, + }, + { + { + FieldElement{12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186}, + FieldElement{2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610}, + FieldElement{-2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707}, + }, + { + FieldElement{7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220}, + FieldElement{915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025}, + FieldElement{32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044}, + }, + { + FieldElement{32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992}, + FieldElement{-4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027}, + FieldElement{21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197}, + }, + { + FieldElement{8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901}, + FieldElement{31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952}, + FieldElement{19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878}, + }, + { + FieldElement{-28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390}, + FieldElement{32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730}, + FieldElement{2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730}, + }, + { + FieldElement{-19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180}, + FieldElement{-30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272}, + FieldElement{-15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715}, + }, + { + FieldElement{-22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970}, + FieldElement{-31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772}, + FieldElement{-17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865}, + }, + { + FieldElement{15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750}, + FieldElement{20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373}, + FieldElement{32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348}, + }, + }, + { + { + FieldElement{9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144}, + FieldElement{-22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195}, + FieldElement{5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086}, + }, + { + FieldElement{-13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684}, + FieldElement{-8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518}, + FieldElement{-2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233}, + }, + { + FieldElement{-5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793}, + FieldElement{-2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794}, + FieldElement{580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435}, + }, + { + FieldElement{23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921}, + FieldElement{13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518}, + FieldElement{2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563}, + }, + { + FieldElement{14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278}, + FieldElement{-27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024}, + FieldElement{4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030}, + }, + { + FieldElement{10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783}, + FieldElement{27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717}, + FieldElement{6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844}, + }, + { + FieldElement{14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333}, + FieldElement{16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048}, + FieldElement{22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760}, + }, + { + FieldElement{-4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760}, + FieldElement{-15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757}, + FieldElement{-2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112}, + }, + }, + { + { + FieldElement{-19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468}, + FieldElement{3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184}, + FieldElement{10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289}, + }, + { + FieldElement{15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066}, + FieldElement{24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882}, + FieldElement{13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226}, + }, + { + FieldElement{16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101}, + FieldElement{29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279}, + FieldElement{-6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811}, + }, + { + FieldElement{27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709}, + FieldElement{20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714}, + FieldElement{-2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121}, + }, + { + FieldElement{9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464}, + FieldElement{12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847}, + FieldElement{13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400}, + }, + { + FieldElement{4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414}, + FieldElement{-15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158}, + FieldElement{17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045}, + }, + { + FieldElement{-461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415}, + FieldElement{-5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459}, + FieldElement{-31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079}, + }, + { + FieldElement{21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412}, + FieldElement{-20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743}, + FieldElement{-14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836}, + }, + }, + { + { + FieldElement{12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022}, + FieldElement{18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429}, + FieldElement{-6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065}, + }, + { + FieldElement{30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861}, + FieldElement{10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000}, + FieldElement{-33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101}, + }, + { + FieldElement{32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815}, + FieldElement{29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642}, + FieldElement{10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966}, + }, + { + FieldElement{25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574}, + FieldElement{-21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742}, + FieldElement{-18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689}, + }, + { + FieldElement{12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020}, + FieldElement{-10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772}, + FieldElement{3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982}, + }, + { + FieldElement{-14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953}, + FieldElement{-16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218}, + FieldElement{-17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265}, + }, + { + FieldElement{29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073}, + FieldElement{-3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325}, + FieldElement{-11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798}, + }, + { + FieldElement{-4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870}, + FieldElement{-7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863}, + FieldElement{-13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927}, + }, + }, + { + { + FieldElement{-2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267}, + FieldElement{-9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663}, + FieldElement{22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862}, + }, + { + FieldElement{-25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673}, + FieldElement{15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943}, + FieldElement{15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020}, + }, + { + FieldElement{-4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238}, + FieldElement{11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064}, + FieldElement{14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795}, + }, + { + FieldElement{15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052}, + FieldElement{-10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904}, + FieldElement{29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531}, + }, + { + FieldElement{-13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979}, + FieldElement{-5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841}, + FieldElement{10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431}, + }, + { + FieldElement{10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324}, + FieldElement{-31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940}, + FieldElement{10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320}, + }, + { + FieldElement{-15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184}, + FieldElement{14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114}, + FieldElement{30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878}, + }, + { + FieldElement{12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784}, + FieldElement{-2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091}, + FieldElement{-16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585}, + }, + }, + { + { + FieldElement{-8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208}, + FieldElement{10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864}, + FieldElement{17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661}, + }, + { + FieldElement{7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233}, + FieldElement{26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212}, + FieldElement{-12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525}, + }, + { + FieldElement{-24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068}, + FieldElement{9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397}, + FieldElement{-8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988}, + }, + { + FieldElement{5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889}, + FieldElement{32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038}, + FieldElement{14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697}, + }, + { + FieldElement{20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875}, + FieldElement{-25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905}, + FieldElement{-25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656}, + }, + { + FieldElement{11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818}, + FieldElement{27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714}, + FieldElement{10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203}, + }, + { + FieldElement{20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931}, + FieldElement{-30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024}, + FieldElement{-23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084}, + }, + { + FieldElement{-1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204}, + FieldElement{20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817}, + FieldElement{27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667}, + }, + }, + { + { + FieldElement{11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504}, + FieldElement{-12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768}, + FieldElement{-19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255}, + }, + { + FieldElement{6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790}, + FieldElement{1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438}, + FieldElement{-22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333}, + }, + { + FieldElement{17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971}, + FieldElement{31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905}, + FieldElement{29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409}, + }, + { + FieldElement{12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409}, + FieldElement{6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499}, + FieldElement{-8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363}, + }, + { + FieldElement{28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664}, + FieldElement{-11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324}, + FieldElement{-21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940}, + }, + { + FieldElement{13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990}, + FieldElement{-17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914}, + FieldElement{-25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290}, + }, + { + FieldElement{24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257}, + FieldElement{-6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433}, + FieldElement{-16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236}, + }, + { + FieldElement{-12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045}, + FieldElement{11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093}, + FieldElement{-1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347}, + }, + }, + { + { + FieldElement{-28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191}, + FieldElement{-15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507}, + FieldElement{-12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906}, + }, + { + FieldElement{3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018}, + FieldElement{-16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109}, + FieldElement{-23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926}, + }, + { + FieldElement{-24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528}, + FieldElement{8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625}, + FieldElement{-32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286}, + }, + { + FieldElement{2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033}, + FieldElement{27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866}, + FieldElement{21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896}, + }, + { + FieldElement{30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075}, + FieldElement{26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347}, + FieldElement{-22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437}, + }, + { + FieldElement{-5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165}, + FieldElement{-18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588}, + FieldElement{-32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193}, + }, + { + FieldElement{-19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017}, + FieldElement{-28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883}, + FieldElement{21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961}, + }, + { + FieldElement{8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043}, + FieldElement{29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663}, + FieldElement{-20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362}, + }, + }, + { + { + FieldElement{-33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860}, + FieldElement{2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466}, + FieldElement{-24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063}, + }, + { + FieldElement{-26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997}, + FieldElement{-1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295}, + FieldElement{-13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369}, + }, + { + FieldElement{9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385}, + FieldElement{18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109}, + FieldElement{2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906}, + }, + { + FieldElement{4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424}, + FieldElement{-19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185}, + FieldElement{7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962}, + }, + { + FieldElement{-7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325}, + FieldElement{10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593}, + FieldElement{696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404}, + }, + { + FieldElement{-11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644}, + FieldElement{17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801}, + FieldElement{26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804}, + }, + { + FieldElement{-31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884}, + FieldElement{-586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577}, + FieldElement{-9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849}, + }, + { + FieldElement{32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473}, + FieldElement{-8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644}, + FieldElement{-2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319}, + }, + }, + { + { + FieldElement{-11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599}, + FieldElement{-9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768}, + FieldElement{-27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084}, + }, + { + FieldElement{-27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328}, + FieldElement{-15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369}, + FieldElement{20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920}, + }, + { + FieldElement{12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815}, + FieldElement{-32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025}, + FieldElement{-21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397}, + }, + { + FieldElement{-20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448}, + FieldElement{6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981}, + FieldElement{30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165}, + }, + { + FieldElement{32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501}, + FieldElement{17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073}, + FieldElement{-1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861}, + }, + { + FieldElement{14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845}, + FieldElement{-1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211}, + FieldElement{18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870}, + }, + { + FieldElement{10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096}, + FieldElement{33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803}, + FieldElement{-32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168}, + }, + { + FieldElement{30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965}, + FieldElement{-14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505}, + FieldElement{18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598}, + }, + }, + { + { + FieldElement{5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782}, + FieldElement{5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900}, + FieldElement{-31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479}, + }, + { + FieldElement{-12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208}, + FieldElement{8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232}, + FieldElement{17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719}, + }, + { + FieldElement{16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271}, + FieldElement{-4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326}, + FieldElement{-8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132}, + }, + { + FieldElement{14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300}, + FieldElement{8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570}, + FieldElement{15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670}, + }, + { + FieldElement{-2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994}, + FieldElement{-12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913}, + FieldElement{31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317}, + }, + { + FieldElement{-25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730}, + FieldElement{842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096}, + FieldElement{-4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078}, + }, + { + FieldElement{-15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411}, + FieldElement{-19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905}, + FieldElement{-9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654}, + }, + { + FieldElement{-28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870}, + FieldElement{-23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498}, + FieldElement{12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579}, + }, + }, + { + { + FieldElement{14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677}, + FieldElement{10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647}, + FieldElement{-2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743}, + }, + { + FieldElement{-25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468}, + FieldElement{21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375}, + FieldElement{-25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155}, + }, + { + FieldElement{6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725}, + FieldElement{-12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612}, + FieldElement{-10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943}, + }, + { + FieldElement{-30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944}, + FieldElement{30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928}, + FieldElement{9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406}, + }, + { + FieldElement{22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139}, + FieldElement{-8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963}, + FieldElement{-31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693}, + }, + { + FieldElement{1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734}, + FieldElement{-448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680}, + FieldElement{-24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410}, + }, + { + FieldElement{-9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931}, + FieldElement{-16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654}, + FieldElement{22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710}, + }, + { + FieldElement{29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180}, + FieldElement{-26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684}, + FieldElement{-10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895}, + }, + }, + { + { + FieldElement{22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501}, + FieldElement{-11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413}, + FieldElement{6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880}, + }, + { + FieldElement{-8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874}, + FieldElement{22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962}, + FieldElement{-7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899}, + }, + { + FieldElement{21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152}, + FieldElement{9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063}, + FieldElement{7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080}, + }, + { + FieldElement{-9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146}, + FieldElement{-17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183}, + FieldElement{-19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133}, + }, + { + FieldElement{-32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421}, + FieldElement{-3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622}, + FieldElement{-4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197}, + }, + { + FieldElement{2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663}, + FieldElement{31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753}, + FieldElement{4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755}, + }, + { + FieldElement{-9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862}, + FieldElement{-26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118}, + FieldElement{26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171}, + }, + { + FieldElement{15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380}, + FieldElement{16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824}, + FieldElement{28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270}, + }, + }, + { + { + FieldElement{-817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438}, + FieldElement{-31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584}, + FieldElement{-594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562}, + }, + { + FieldElement{30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471}, + FieldElement{18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610}, + FieldElement{19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269}, + }, + { + FieldElement{-30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650}, + FieldElement{14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369}, + FieldElement{19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461}, + }, + { + FieldElement{30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462}, + FieldElement{-5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793}, + FieldElement{-2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218}, + }, + { + FieldElement{-24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226}, + FieldElement{18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019}, + FieldElement{-15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037}, + }, + { + FieldElement{31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171}, + FieldElement{-17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132}, + FieldElement{-28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841}, + }, + { + FieldElement{21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181}, + FieldElement{-33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210}, + FieldElement{-1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040}, + }, + { + FieldElement{3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935}, + FieldElement{24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105}, + FieldElement{-28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814}, + }, + }, + { + { + FieldElement{793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852}, + FieldElement{5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581}, + FieldElement{-4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646}, + }, + { + FieldElement{10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844}, + FieldElement{10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025}, + FieldElement{27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453}, + }, + { + FieldElement{-23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068}, + FieldElement{4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192}, + FieldElement{-17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921}, + }, + { + FieldElement{-9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259}, + FieldElement{-12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426}, + FieldElement{-5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072}, + }, + { + FieldElement{-17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305}, + FieldElement{13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832}, + FieldElement{28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943}, + }, + { + FieldElement{-16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011}, + FieldElement{24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447}, + FieldElement{17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494}, + }, + { + FieldElement{-28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245}, + FieldElement{-20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859}, + FieldElement{28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915}, + }, + { + FieldElement{16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707}, + FieldElement{10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848}, + FieldElement{-11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224}, + }, + }, + { + { + FieldElement{-25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391}, + FieldElement{15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215}, + FieldElement{-23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101}, + }, + { + FieldElement{23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713}, + FieldElement{21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849}, + FieldElement{-7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930}, + }, + { + FieldElement{-29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940}, + FieldElement{-21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031}, + FieldElement{-17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404}, + }, + { + FieldElement{-25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243}, + FieldElement{-23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116}, + FieldElement{-24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525}, + }, + { + FieldElement{-23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509}, + FieldElement{-10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883}, + FieldElement{15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865}, + }, + { + FieldElement{-3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660}, + FieldElement{4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273}, + FieldElement{-28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138}, + }, + { + FieldElement{-25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560}, + FieldElement{-10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135}, + FieldElement{2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941}, + }, + { + FieldElement{-4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739}, + FieldElement{18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756}, + FieldElement{-30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819}, + }, + }, + { + { + FieldElement{-6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347}, + FieldElement{-27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028}, + FieldElement{21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075}, + }, + { + FieldElement{16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799}, + FieldElement{-2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609}, + FieldElement{-25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817}, + }, + { + FieldElement{-23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989}, + FieldElement{-30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523}, + FieldElement{4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278}, + }, + { + FieldElement{31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045}, + FieldElement{19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377}, + FieldElement{24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480}, + }, + { + FieldElement{17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016}, + FieldElement{510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426}, + FieldElement{18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525}, + }, + { + FieldElement{13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396}, + FieldElement{9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080}, + FieldElement{12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892}, + }, + { + FieldElement{15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275}, + FieldElement{11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074}, + FieldElement{20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140}, + }, + { + FieldElement{-16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717}, + FieldElement{-1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101}, + FieldElement{24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127}, + }, + }, + { + { + FieldElement{-12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632}, + FieldElement{-26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415}, + FieldElement{-31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160}, + }, + { + FieldElement{31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876}, + FieldElement{22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625}, + FieldElement{-15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478}, + }, + { + FieldElement{27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164}, + FieldElement{26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595}, + FieldElement{-7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248}, + }, + { + FieldElement{-16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858}, + FieldElement{15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193}, + FieldElement{8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184}, + }, + { + FieldElement{-18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942}, + FieldElement{-1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635}, + FieldElement{21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948}, + }, + { + FieldElement{11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935}, + FieldElement{-25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415}, + FieldElement{-15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416}, + }, + { + FieldElement{-7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018}, + FieldElement{4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778}, + FieldElement{366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659}, + }, + { + FieldElement{-24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385}, + FieldElement{18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503}, + FieldElement{476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329}, + }, + }, + { + { + FieldElement{20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056}, + FieldElement{-13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838}, + FieldElement{24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948}, + }, + { + FieldElement{-3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691}, + FieldElement{-15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118}, + FieldElement{-23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517}, + }, + { + FieldElement{-20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269}, + FieldElement{-6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904}, + FieldElement{-23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589}, + }, + { + FieldElement{-28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193}, + FieldElement{-7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910}, + FieldElement{-30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930}, + }, + { + FieldElement{-7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667}, + FieldElement{25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481}, + FieldElement{-9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876}, + }, + { + FieldElement{22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640}, + FieldElement{-8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278}, + FieldElement{-21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112}, + }, + { + FieldElement{26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272}, + FieldElement{17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012}, + FieldElement{-10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221}, + }, + { + FieldElement{30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046}, + FieldElement{13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345}, + FieldElement{-19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310}, + }, + }, + { + { + FieldElement{19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937}, + FieldElement{31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636}, + FieldElement{-9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008}, + }, + { + FieldElement{-2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429}, + FieldElement{-15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576}, + FieldElement{31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066}, + }, + { + FieldElement{-9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490}, + FieldElement{-12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104}, + FieldElement{33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053}, + }, + { + FieldElement{31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275}, + FieldElement{-20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511}, + FieldElement{22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095}, + }, + { + FieldElement{-28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439}, + FieldElement{23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939}, + FieldElement{-23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424}, + }, + { + FieldElement{2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310}, + FieldElement{3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608}, + FieldElement{-32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079}, + }, + { + FieldElement{-23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101}, + FieldElement{21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418}, + FieldElement{18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576}, + }, + { + FieldElement{30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356}, + FieldElement{9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996}, + FieldElement{-26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099}, + }, + }, + { + { + FieldElement{-26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728}, + FieldElement{-13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658}, + FieldElement{-10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242}, + }, + { + FieldElement{-21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001}, + FieldElement{-4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766}, + FieldElement{18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373}, + }, + { + FieldElement{26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458}, + FieldElement{-17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628}, + FieldElement{-13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657}, + }, + { + FieldElement{-23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062}, + FieldElement{25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616}, + FieldElement{31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014}, + }, + { + FieldElement{24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383}, + FieldElement{-25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814}, + FieldElement{-20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718}, + }, + { + FieldElement{30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417}, + FieldElement{2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222}, + FieldElement{33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444}, + }, + { + FieldElement{-20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597}, + FieldElement{23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970}, + FieldElement{1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799}, + }, + { + FieldElement{-5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647}, + FieldElement{13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511}, + FieldElement{-29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032}, + }, + }, + { + { + FieldElement{9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834}, + FieldElement{-23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461}, + FieldElement{29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062}, + }, + { + FieldElement{-25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516}, + FieldElement{-20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547}, + FieldElement{-24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240}, + }, + { + FieldElement{-17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038}, + FieldElement{-33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741}, + FieldElement{16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103}, + }, + { + FieldElement{-19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747}, + FieldElement{-1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323}, + FieldElement{31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016}, + }, + { + FieldElement{-14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373}, + FieldElement{15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228}, + FieldElement{-2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141}, + }, + { + FieldElement{16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399}, + FieldElement{11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831}, + FieldElement{-185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376}, + }, + { + FieldElement{-32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313}, + FieldElement{-18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958}, + FieldElement{-6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577}, + }, + { + FieldElement{-22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743}, + FieldElement{29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684}, + FieldElement{-20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476}, + }, + }, +} diff --git a/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go b/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go new file mode 100644 index 00000000000..fd03c252af4 --- /dev/null +++ b/vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go @@ -0,0 +1,1793 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package edwards25519 + +import "encoding/binary" + +// This code is a port of the public domain, “ref10” implementation of ed25519 +// from SUPERCOP. + +// FieldElement represents an element of the field GF(2^255 - 19). An element +// t, entries t[0]...t[9], represents the integer t[0]+2^26 t[1]+2^51 t[2]+2^77 +// t[3]+2^102 t[4]+...+2^230 t[9]. Bounds on each t[i] vary depending on +// context. +type FieldElement [10]int32 + +var zero FieldElement + +func FeZero(fe *FieldElement) { + copy(fe[:], zero[:]) +} + +func FeOne(fe *FieldElement) { + FeZero(fe) + fe[0] = 1 +} + +func FeAdd(dst, a, b *FieldElement) { + dst[0] = a[0] + b[0] + dst[1] = a[1] + b[1] + dst[2] = a[2] + b[2] + dst[3] = a[3] + b[3] + dst[4] = a[4] + b[4] + dst[5] = a[5] + b[5] + dst[6] = a[6] + b[6] + dst[7] = a[7] + b[7] + dst[8] = a[8] + b[8] + dst[9] = a[9] + b[9] +} + +func FeSub(dst, a, b *FieldElement) { + dst[0] = a[0] - b[0] + dst[1] = a[1] - b[1] + dst[2] = a[2] - b[2] + dst[3] = a[3] - b[3] + dst[4] = a[4] - b[4] + dst[5] = a[5] - b[5] + dst[6] = a[6] - b[6] + dst[7] = a[7] - b[7] + dst[8] = a[8] - b[8] + dst[9] = a[9] - b[9] +} + +func FeCopy(dst, src *FieldElement) { + copy(dst[:], src[:]) +} + +// Replace (f,g) with (g,g) if b == 1; +// replace (f,g) with (f,g) if b == 0. +// +// Preconditions: b in {0,1}. +func FeCMove(f, g *FieldElement, b int32) { + b = -b + f[0] ^= b & (f[0] ^ g[0]) + f[1] ^= b & (f[1] ^ g[1]) + f[2] ^= b & (f[2] ^ g[2]) + f[3] ^= b & (f[3] ^ g[3]) + f[4] ^= b & (f[4] ^ g[4]) + f[5] ^= b & (f[5] ^ g[5]) + f[6] ^= b & (f[6] ^ g[6]) + f[7] ^= b & (f[7] ^ g[7]) + f[8] ^= b & (f[8] ^ g[8]) + f[9] ^= b & (f[9] ^ g[9]) +} + +func load3(in []byte) int64 { + var r int64 + r = int64(in[0]) + r |= int64(in[1]) << 8 + r |= int64(in[2]) << 16 + return r +} + +func load4(in []byte) int64 { + var r int64 + r = int64(in[0]) + r |= int64(in[1]) << 8 + r |= int64(in[2]) << 16 + r |= int64(in[3]) << 24 + return r +} + +func FeFromBytes(dst *FieldElement, src *[32]byte) { + h0 := load4(src[:]) + h1 := load3(src[4:]) << 6 + h2 := load3(src[7:]) << 5 + h3 := load3(src[10:]) << 3 + h4 := load3(src[13:]) << 2 + h5 := load4(src[16:]) + h6 := load3(src[20:]) << 7 + h7 := load3(src[23:]) << 5 + h8 := load3(src[26:]) << 4 + h9 := (load3(src[29:]) & 8388607) << 2 + + FeCombine(dst, h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) +} + +// FeToBytes marshals h to s. +// Preconditions: +// |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +// +// Write p=2^255-19; q=floor(h/p). +// Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). +// +// Proof: +// Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. +// Also have |h-2^230 h9|<2^230 so |19 2^(-255)(h-2^230 h9)|<1/4. +// +// Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). +// Then 0> 25 + q = (h[0] + q) >> 26 + q = (h[1] + q) >> 25 + q = (h[2] + q) >> 26 + q = (h[3] + q) >> 25 + q = (h[4] + q) >> 26 + q = (h[5] + q) >> 25 + q = (h[6] + q) >> 26 + q = (h[7] + q) >> 25 + q = (h[8] + q) >> 26 + q = (h[9] + q) >> 25 + + // Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. + h[0] += 19 * q + // Goal: Output h-2^255 q, which is between 0 and 2^255-20. + + carry[0] = h[0] >> 26 + h[1] += carry[0] + h[0] -= carry[0] << 26 + carry[1] = h[1] >> 25 + h[2] += carry[1] + h[1] -= carry[1] << 25 + carry[2] = h[2] >> 26 + h[3] += carry[2] + h[2] -= carry[2] << 26 + carry[3] = h[3] >> 25 + h[4] += carry[3] + h[3] -= carry[3] << 25 + carry[4] = h[4] >> 26 + h[5] += carry[4] + h[4] -= carry[4] << 26 + carry[5] = h[5] >> 25 + h[6] += carry[5] + h[5] -= carry[5] << 25 + carry[6] = h[6] >> 26 + h[7] += carry[6] + h[6] -= carry[6] << 26 + carry[7] = h[7] >> 25 + h[8] += carry[7] + h[7] -= carry[7] << 25 + carry[8] = h[8] >> 26 + h[9] += carry[8] + h[8] -= carry[8] << 26 + carry[9] = h[9] >> 25 + h[9] -= carry[9] << 25 + // h10 = carry9 + + // Goal: Output h[0]+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + // Have h[0]+...+2^230 h[9] between 0 and 2^255-1; + // evidently 2^255 h10-2^255 q = 0. + // Goal: Output h[0]+...+2^230 h[9]. + + s[0] = byte(h[0] >> 0) + s[1] = byte(h[0] >> 8) + s[2] = byte(h[0] >> 16) + s[3] = byte((h[0] >> 24) | (h[1] << 2)) + s[4] = byte(h[1] >> 6) + s[5] = byte(h[1] >> 14) + s[6] = byte((h[1] >> 22) | (h[2] << 3)) + s[7] = byte(h[2] >> 5) + s[8] = byte(h[2] >> 13) + s[9] = byte((h[2] >> 21) | (h[3] << 5)) + s[10] = byte(h[3] >> 3) + s[11] = byte(h[3] >> 11) + s[12] = byte((h[3] >> 19) | (h[4] << 6)) + s[13] = byte(h[4] >> 2) + s[14] = byte(h[4] >> 10) + s[15] = byte(h[4] >> 18) + s[16] = byte(h[5] >> 0) + s[17] = byte(h[5] >> 8) + s[18] = byte(h[5] >> 16) + s[19] = byte((h[5] >> 24) | (h[6] << 1)) + s[20] = byte(h[6] >> 7) + s[21] = byte(h[6] >> 15) + s[22] = byte((h[6] >> 23) | (h[7] << 3)) + s[23] = byte(h[7] >> 5) + s[24] = byte(h[7] >> 13) + s[25] = byte((h[7] >> 21) | (h[8] << 4)) + s[26] = byte(h[8] >> 4) + s[27] = byte(h[8] >> 12) + s[28] = byte((h[8] >> 20) | (h[9] << 6)) + s[29] = byte(h[9] >> 2) + s[30] = byte(h[9] >> 10) + s[31] = byte(h[9] >> 18) +} + +func FeIsNegative(f *FieldElement) byte { + var s [32]byte + FeToBytes(&s, f) + return s[0] & 1 +} + +func FeIsNonZero(f *FieldElement) int32 { + var s [32]byte + FeToBytes(&s, f) + var x uint8 + for _, b := range s { + x |= b + } + x |= x >> 4 + x |= x >> 2 + x |= x >> 1 + return int32(x & 1) +} + +// FeNeg sets h = -f +// +// Preconditions: +// |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +// +// Postconditions: +// |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +func FeNeg(h, f *FieldElement) { + h[0] = -f[0] + h[1] = -f[1] + h[2] = -f[2] + h[3] = -f[3] + h[4] = -f[4] + h[5] = -f[5] + h[6] = -f[6] + h[7] = -f[7] + h[8] = -f[8] + h[9] = -f[9] +} + +func FeCombine(h *FieldElement, h0, h1, h2, h3, h4, h5, h6, h7, h8, h9 int64) { + var c0, c1, c2, c3, c4, c5, c6, c7, c8, c9 int64 + + /* + |h0| <= (1.1*1.1*2^52*(1+19+19+19+19)+1.1*1.1*2^50*(38+38+38+38+38)) + i.e. |h0| <= 1.2*2^59; narrower ranges for h2, h4, h6, h8 + |h1| <= (1.1*1.1*2^51*(1+1+19+19+19+19+19+19+19+19)) + i.e. |h1| <= 1.5*2^58; narrower ranges for h3, h5, h7, h9 + */ + + c0 = (h0 + (1 << 25)) >> 26 + h1 += c0 + h0 -= c0 << 26 + c4 = (h4 + (1 << 25)) >> 26 + h5 += c4 + h4 -= c4 << 26 + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.51*2^58 */ + /* |h5| <= 1.51*2^58 */ + + c1 = (h1 + (1 << 24)) >> 25 + h2 += c1 + h1 -= c1 << 25 + c5 = (h5 + (1 << 24)) >> 25 + h6 += c5 + h5 -= c5 << 25 + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.21*2^59 */ + /* |h6| <= 1.21*2^59 */ + + c2 = (h2 + (1 << 25)) >> 26 + h3 += c2 + h2 -= c2 << 26 + c6 = (h6 + (1 << 25)) >> 26 + h7 += c6 + h6 -= c6 << 26 + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.51*2^58 */ + /* |h7| <= 1.51*2^58 */ + + c3 = (h3 + (1 << 24)) >> 25 + h4 += c3 + h3 -= c3 << 25 + c7 = (h7 + (1 << 24)) >> 25 + h8 += c7 + h7 -= c7 << 25 + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.52*2^33 */ + /* |h8| <= 1.52*2^33 */ + + c4 = (h4 + (1 << 25)) >> 26 + h5 += c4 + h4 -= c4 << 26 + c8 = (h8 + (1 << 25)) >> 26 + h9 += c8 + h8 -= c8 << 26 + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.51*2^58 */ + + c9 = (h9 + (1 << 24)) >> 25 + h0 += c9 * 19 + h9 -= c9 << 25 + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.8*2^37 */ + + c0 = (h0 + (1 << 25)) >> 26 + h1 += c0 + h0 -= c0 << 26 + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + h[0] = int32(h0) + h[1] = int32(h1) + h[2] = int32(h2) + h[3] = int32(h3) + h[4] = int32(h4) + h[5] = int32(h5) + h[6] = int32(h6) + h[7] = int32(h7) + h[8] = int32(h8) + h[9] = int32(h9) +} + +// FeMul calculates h = f * g +// Can overlap h with f or g. +// +// Preconditions: +// |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +// |g| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +// +// Postconditions: +// |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +// +// Notes on implementation strategy: +// +// Using schoolbook multiplication. +// Karatsuba would save a little in some cost models. +// +// Most multiplications by 2 and 19 are 32-bit precomputations; +// cheaper than 64-bit postcomputations. +// +// There is one remaining multiplication by 19 in the carry chain; +// one *19 precomputation can be merged into this, +// but the resulting data flow is considerably less clean. +// +// There are 12 carries below. +// 10 of them are 2-way parallelizable and vectorizable. +// Can get away with 11 carries, but then data flow is much deeper. +// +// With tighter constraints on inputs, can squeeze carries into int32. +func FeMul(h, f, g *FieldElement) { + f0 := int64(f[0]) + f1 := int64(f[1]) + f2 := int64(f[2]) + f3 := int64(f[3]) + f4 := int64(f[4]) + f5 := int64(f[5]) + f6 := int64(f[6]) + f7 := int64(f[7]) + f8 := int64(f[8]) + f9 := int64(f[9]) + + f1_2 := int64(2 * f[1]) + f3_2 := int64(2 * f[3]) + f5_2 := int64(2 * f[5]) + f7_2 := int64(2 * f[7]) + f9_2 := int64(2 * f[9]) + + g0 := int64(g[0]) + g1 := int64(g[1]) + g2 := int64(g[2]) + g3 := int64(g[3]) + g4 := int64(g[4]) + g5 := int64(g[5]) + g6 := int64(g[6]) + g7 := int64(g[7]) + g8 := int64(g[8]) + g9 := int64(g[9]) + + g1_19 := int64(19 * g[1]) /* 1.4*2^29 */ + g2_19 := int64(19 * g[2]) /* 1.4*2^30; still ok */ + g3_19 := int64(19 * g[3]) + g4_19 := int64(19 * g[4]) + g5_19 := int64(19 * g[5]) + g6_19 := int64(19 * g[6]) + g7_19 := int64(19 * g[7]) + g8_19 := int64(19 * g[8]) + g9_19 := int64(19 * g[9]) + + h0 := f0*g0 + f1_2*g9_19 + f2*g8_19 + f3_2*g7_19 + f4*g6_19 + f5_2*g5_19 + f6*g4_19 + f7_2*g3_19 + f8*g2_19 + f9_2*g1_19 + h1 := f0*g1 + f1*g0 + f2*g9_19 + f3*g8_19 + f4*g7_19 + f5*g6_19 + f6*g5_19 + f7*g4_19 + f8*g3_19 + f9*g2_19 + h2 := f0*g2 + f1_2*g1 + f2*g0 + f3_2*g9_19 + f4*g8_19 + f5_2*g7_19 + f6*g6_19 + f7_2*g5_19 + f8*g4_19 + f9_2*g3_19 + h3 := f0*g3 + f1*g2 + f2*g1 + f3*g0 + f4*g9_19 + f5*g8_19 + f6*g7_19 + f7*g6_19 + f8*g5_19 + f9*g4_19 + h4 := f0*g4 + f1_2*g3 + f2*g2 + f3_2*g1 + f4*g0 + f5_2*g9_19 + f6*g8_19 + f7_2*g7_19 + f8*g6_19 + f9_2*g5_19 + h5 := f0*g5 + f1*g4 + f2*g3 + f3*g2 + f4*g1 + f5*g0 + f6*g9_19 + f7*g8_19 + f8*g7_19 + f9*g6_19 + h6 := f0*g6 + f1_2*g5 + f2*g4 + f3_2*g3 + f4*g2 + f5_2*g1 + f6*g0 + f7_2*g9_19 + f8*g8_19 + f9_2*g7_19 + h7 := f0*g7 + f1*g6 + f2*g5 + f3*g4 + f4*g3 + f5*g2 + f6*g1 + f7*g0 + f8*g9_19 + f9*g8_19 + h8 := f0*g8 + f1_2*g7 + f2*g6 + f3_2*g5 + f4*g4 + f5_2*g3 + f6*g2 + f7_2*g1 + f8*g0 + f9_2*g9_19 + h9 := f0*g9 + f1*g8 + f2*g7 + f3*g6 + f4*g5 + f5*g4 + f6*g3 + f7*g2 + f8*g1 + f9*g0 + + FeCombine(h, h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) +} + +func feSquare(f *FieldElement) (h0, h1, h2, h3, h4, h5, h6, h7, h8, h9 int64) { + f0 := int64(f[0]) + f1 := int64(f[1]) + f2 := int64(f[2]) + f3 := int64(f[3]) + f4 := int64(f[4]) + f5 := int64(f[5]) + f6 := int64(f[6]) + f7 := int64(f[7]) + f8 := int64(f[8]) + f9 := int64(f[9]) + f0_2 := int64(2 * f[0]) + f1_2 := int64(2 * f[1]) + f2_2 := int64(2 * f[2]) + f3_2 := int64(2 * f[3]) + f4_2 := int64(2 * f[4]) + f5_2 := int64(2 * f[5]) + f6_2 := int64(2 * f[6]) + f7_2 := int64(2 * f[7]) + f5_38 := 38 * f5 // 1.31*2^30 + f6_19 := 19 * f6 // 1.31*2^30 + f7_38 := 38 * f7 // 1.31*2^30 + f8_19 := 19 * f8 // 1.31*2^30 + f9_38 := 38 * f9 // 1.31*2^30 + + h0 = f0*f0 + f1_2*f9_38 + f2_2*f8_19 + f3_2*f7_38 + f4_2*f6_19 + f5*f5_38 + h1 = f0_2*f1 + f2*f9_38 + f3_2*f8_19 + f4*f7_38 + f5_2*f6_19 + h2 = f0_2*f2 + f1_2*f1 + f3_2*f9_38 + f4_2*f8_19 + f5_2*f7_38 + f6*f6_19 + h3 = f0_2*f3 + f1_2*f2 + f4*f9_38 + f5_2*f8_19 + f6*f7_38 + h4 = f0_2*f4 + f1_2*f3_2 + f2*f2 + f5_2*f9_38 + f6_2*f8_19 + f7*f7_38 + h5 = f0_2*f5 + f1_2*f4 + f2_2*f3 + f6*f9_38 + f7_2*f8_19 + h6 = f0_2*f6 + f1_2*f5_2 + f2_2*f4 + f3_2*f3 + f7_2*f9_38 + f8*f8_19 + h7 = f0_2*f7 + f1_2*f6 + f2_2*f5 + f3_2*f4 + f8*f9_38 + h8 = f0_2*f8 + f1_2*f7_2 + f2_2*f6 + f3_2*f5_2 + f4*f4 + f9*f9_38 + h9 = f0_2*f9 + f1_2*f8 + f2_2*f7 + f3_2*f6 + f4_2*f5 + + return +} + +// FeSquare calculates h = f*f. Can overlap h with f. +// +// Preconditions: +// |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +// +// Postconditions: +// |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +func FeSquare(h, f *FieldElement) { + h0, h1, h2, h3, h4, h5, h6, h7, h8, h9 := feSquare(f) + FeCombine(h, h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) +} + +// FeSquare2 sets h = 2 * f * f +// +// Can overlap h with f. +// +// Preconditions: +// |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. +// +// Postconditions: +// |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +// See fe_mul.c for discussion of implementation strategy. +func FeSquare2(h, f *FieldElement) { + h0, h1, h2, h3, h4, h5, h6, h7, h8, h9 := feSquare(f) + + h0 += h0 + h1 += h1 + h2 += h2 + h3 += h3 + h4 += h4 + h5 += h5 + h6 += h6 + h7 += h7 + h8 += h8 + h9 += h9 + + FeCombine(h, h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) +} + +func FeInvert(out, z *FieldElement) { + var t0, t1, t2, t3 FieldElement + var i int + + FeSquare(&t0, z) // 2^1 + FeSquare(&t1, &t0) // 2^2 + for i = 1; i < 2; i++ { // 2^3 + FeSquare(&t1, &t1) + } + FeMul(&t1, z, &t1) // 2^3 + 2^0 + FeMul(&t0, &t0, &t1) // 2^3 + 2^1 + 2^0 + FeSquare(&t2, &t0) // 2^4 + 2^2 + 2^1 + FeMul(&t1, &t1, &t2) // 2^4 + 2^3 + 2^2 + 2^1 + 2^0 + FeSquare(&t2, &t1) // 5,4,3,2,1 + for i = 1; i < 5; i++ { // 9,8,7,6,5 + FeSquare(&t2, &t2) + } + FeMul(&t1, &t2, &t1) // 9,8,7,6,5,4,3,2,1,0 + FeSquare(&t2, &t1) // 10..1 + for i = 1; i < 10; i++ { // 19..10 + FeSquare(&t2, &t2) + } + FeMul(&t2, &t2, &t1) // 19..0 + FeSquare(&t3, &t2) // 20..1 + for i = 1; i < 20; i++ { // 39..20 + FeSquare(&t3, &t3) + } + FeMul(&t2, &t3, &t2) // 39..0 + FeSquare(&t2, &t2) // 40..1 + for i = 1; i < 10; i++ { // 49..10 + FeSquare(&t2, &t2) + } + FeMul(&t1, &t2, &t1) // 49..0 + FeSquare(&t2, &t1) // 50..1 + for i = 1; i < 50; i++ { // 99..50 + FeSquare(&t2, &t2) + } + FeMul(&t2, &t2, &t1) // 99..0 + FeSquare(&t3, &t2) // 100..1 + for i = 1; i < 100; i++ { // 199..100 + FeSquare(&t3, &t3) + } + FeMul(&t2, &t3, &t2) // 199..0 + FeSquare(&t2, &t2) // 200..1 + for i = 1; i < 50; i++ { // 249..50 + FeSquare(&t2, &t2) + } + FeMul(&t1, &t2, &t1) // 249..0 + FeSquare(&t1, &t1) // 250..1 + for i = 1; i < 5; i++ { // 254..5 + FeSquare(&t1, &t1) + } + FeMul(out, &t1, &t0) // 254..5,3,1,0 +} + +func fePow22523(out, z *FieldElement) { + var t0, t1, t2 FieldElement + var i int + + FeSquare(&t0, z) + for i = 1; i < 1; i++ { + FeSquare(&t0, &t0) + } + FeSquare(&t1, &t0) + for i = 1; i < 2; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t1, z, &t1) + FeMul(&t0, &t0, &t1) + FeSquare(&t0, &t0) + for i = 1; i < 1; i++ { + FeSquare(&t0, &t0) + } + FeMul(&t0, &t1, &t0) + FeSquare(&t1, &t0) + for i = 1; i < 5; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t0, &t1, &t0) + FeSquare(&t1, &t0) + for i = 1; i < 10; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t1, &t1, &t0) + FeSquare(&t2, &t1) + for i = 1; i < 20; i++ { + FeSquare(&t2, &t2) + } + FeMul(&t1, &t2, &t1) + FeSquare(&t1, &t1) + for i = 1; i < 10; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t0, &t1, &t0) + FeSquare(&t1, &t0) + for i = 1; i < 50; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t1, &t1, &t0) + FeSquare(&t2, &t1) + for i = 1; i < 100; i++ { + FeSquare(&t2, &t2) + } + FeMul(&t1, &t2, &t1) + FeSquare(&t1, &t1) + for i = 1; i < 50; i++ { + FeSquare(&t1, &t1) + } + FeMul(&t0, &t1, &t0) + FeSquare(&t0, &t0) + for i = 1; i < 2; i++ { + FeSquare(&t0, &t0) + } + FeMul(out, &t0, z) +} + +// Group elements are members of the elliptic curve -x^2 + y^2 = 1 + d * x^2 * +// y^2 where d = -121665/121666. +// +// Several representations are used: +// ProjectiveGroupElement: (X:Y:Z) satisfying x=X/Z, y=Y/Z +// ExtendedGroupElement: (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT +// CompletedGroupElement: ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T +// PreComputedGroupElement: (y+x,y-x,2dxy) + +type ProjectiveGroupElement struct { + X, Y, Z FieldElement +} + +type ExtendedGroupElement struct { + X, Y, Z, T FieldElement +} + +type CompletedGroupElement struct { + X, Y, Z, T FieldElement +} + +type PreComputedGroupElement struct { + yPlusX, yMinusX, xy2d FieldElement +} + +type CachedGroupElement struct { + yPlusX, yMinusX, Z, T2d FieldElement +} + +func (p *ProjectiveGroupElement) Zero() { + FeZero(&p.X) + FeOne(&p.Y) + FeOne(&p.Z) +} + +func (p *ProjectiveGroupElement) Double(r *CompletedGroupElement) { + var t0 FieldElement + + FeSquare(&r.X, &p.X) + FeSquare(&r.Z, &p.Y) + FeSquare2(&r.T, &p.Z) + FeAdd(&r.Y, &p.X, &p.Y) + FeSquare(&t0, &r.Y) + FeAdd(&r.Y, &r.Z, &r.X) + FeSub(&r.Z, &r.Z, &r.X) + FeSub(&r.X, &t0, &r.Y) + FeSub(&r.T, &r.T, &r.Z) +} + +func (p *ProjectiveGroupElement) ToBytes(s *[32]byte) { + var recip, x, y FieldElement + + FeInvert(&recip, &p.Z) + FeMul(&x, &p.X, &recip) + FeMul(&y, &p.Y, &recip) + FeToBytes(s, &y) + s[31] ^= FeIsNegative(&x) << 7 +} + +func (p *ExtendedGroupElement) Zero() { + FeZero(&p.X) + FeOne(&p.Y) + FeOne(&p.Z) + FeZero(&p.T) +} + +func (p *ExtendedGroupElement) Double(r *CompletedGroupElement) { + var q ProjectiveGroupElement + p.ToProjective(&q) + q.Double(r) +} + +func (p *ExtendedGroupElement) ToCached(r *CachedGroupElement) { + FeAdd(&r.yPlusX, &p.Y, &p.X) + FeSub(&r.yMinusX, &p.Y, &p.X) + FeCopy(&r.Z, &p.Z) + FeMul(&r.T2d, &p.T, &d2) +} + +func (p *ExtendedGroupElement) ToProjective(r *ProjectiveGroupElement) { + FeCopy(&r.X, &p.X) + FeCopy(&r.Y, &p.Y) + FeCopy(&r.Z, &p.Z) +} + +func (p *ExtendedGroupElement) ToBytes(s *[32]byte) { + var recip, x, y FieldElement + + FeInvert(&recip, &p.Z) + FeMul(&x, &p.X, &recip) + FeMul(&y, &p.Y, &recip) + FeToBytes(s, &y) + s[31] ^= FeIsNegative(&x) << 7 +} + +func (p *ExtendedGroupElement) FromBytes(s *[32]byte) bool { + var u, v, v3, vxx, check FieldElement + + FeFromBytes(&p.Y, s) + FeOne(&p.Z) + FeSquare(&u, &p.Y) + FeMul(&v, &u, &d) + FeSub(&u, &u, &p.Z) // y = y^2-1 + FeAdd(&v, &v, &p.Z) // v = dy^2+1 + + FeSquare(&v3, &v) + FeMul(&v3, &v3, &v) // v3 = v^3 + FeSquare(&p.X, &v3) + FeMul(&p.X, &p.X, &v) + FeMul(&p.X, &p.X, &u) // x = uv^7 + + fePow22523(&p.X, &p.X) // x = (uv^7)^((q-5)/8) + FeMul(&p.X, &p.X, &v3) + FeMul(&p.X, &p.X, &u) // x = uv^3(uv^7)^((q-5)/8) + + var tmpX, tmp2 [32]byte + + FeSquare(&vxx, &p.X) + FeMul(&vxx, &vxx, &v) + FeSub(&check, &vxx, &u) // vx^2-u + if FeIsNonZero(&check) == 1 { + FeAdd(&check, &vxx, &u) // vx^2+u + if FeIsNonZero(&check) == 1 { + return false + } + FeMul(&p.X, &p.X, &SqrtM1) + + FeToBytes(&tmpX, &p.X) + for i, v := range tmpX { + tmp2[31-i] = v + } + } + + if FeIsNegative(&p.X) != (s[31] >> 7) { + FeNeg(&p.X, &p.X) + } + + FeMul(&p.T, &p.X, &p.Y) + return true +} + +func (p *CompletedGroupElement) ToProjective(r *ProjectiveGroupElement) { + FeMul(&r.X, &p.X, &p.T) + FeMul(&r.Y, &p.Y, &p.Z) + FeMul(&r.Z, &p.Z, &p.T) +} + +func (p *CompletedGroupElement) ToExtended(r *ExtendedGroupElement) { + FeMul(&r.X, &p.X, &p.T) + FeMul(&r.Y, &p.Y, &p.Z) + FeMul(&r.Z, &p.Z, &p.T) + FeMul(&r.T, &p.X, &p.Y) +} + +func (p *PreComputedGroupElement) Zero() { + FeOne(&p.yPlusX) + FeOne(&p.yMinusX) + FeZero(&p.xy2d) +} + +func geAdd(r *CompletedGroupElement, p *ExtendedGroupElement, q *CachedGroupElement) { + var t0 FieldElement + + FeAdd(&r.X, &p.Y, &p.X) + FeSub(&r.Y, &p.Y, &p.X) + FeMul(&r.Z, &r.X, &q.yPlusX) + FeMul(&r.Y, &r.Y, &q.yMinusX) + FeMul(&r.T, &q.T2d, &p.T) + FeMul(&r.X, &p.Z, &q.Z) + FeAdd(&t0, &r.X, &r.X) + FeSub(&r.X, &r.Z, &r.Y) + FeAdd(&r.Y, &r.Z, &r.Y) + FeAdd(&r.Z, &t0, &r.T) + FeSub(&r.T, &t0, &r.T) +} + +func geSub(r *CompletedGroupElement, p *ExtendedGroupElement, q *CachedGroupElement) { + var t0 FieldElement + + FeAdd(&r.X, &p.Y, &p.X) + FeSub(&r.Y, &p.Y, &p.X) + FeMul(&r.Z, &r.X, &q.yMinusX) + FeMul(&r.Y, &r.Y, &q.yPlusX) + FeMul(&r.T, &q.T2d, &p.T) + FeMul(&r.X, &p.Z, &q.Z) + FeAdd(&t0, &r.X, &r.X) + FeSub(&r.X, &r.Z, &r.Y) + FeAdd(&r.Y, &r.Z, &r.Y) + FeSub(&r.Z, &t0, &r.T) + FeAdd(&r.T, &t0, &r.T) +} + +func geMixedAdd(r *CompletedGroupElement, p *ExtendedGroupElement, q *PreComputedGroupElement) { + var t0 FieldElement + + FeAdd(&r.X, &p.Y, &p.X) + FeSub(&r.Y, &p.Y, &p.X) + FeMul(&r.Z, &r.X, &q.yPlusX) + FeMul(&r.Y, &r.Y, &q.yMinusX) + FeMul(&r.T, &q.xy2d, &p.T) + FeAdd(&t0, &p.Z, &p.Z) + FeSub(&r.X, &r.Z, &r.Y) + FeAdd(&r.Y, &r.Z, &r.Y) + FeAdd(&r.Z, &t0, &r.T) + FeSub(&r.T, &t0, &r.T) +} + +func geMixedSub(r *CompletedGroupElement, p *ExtendedGroupElement, q *PreComputedGroupElement) { + var t0 FieldElement + + FeAdd(&r.X, &p.Y, &p.X) + FeSub(&r.Y, &p.Y, &p.X) + FeMul(&r.Z, &r.X, &q.yMinusX) + FeMul(&r.Y, &r.Y, &q.yPlusX) + FeMul(&r.T, &q.xy2d, &p.T) + FeAdd(&t0, &p.Z, &p.Z) + FeSub(&r.X, &r.Z, &r.Y) + FeAdd(&r.Y, &r.Z, &r.Y) + FeSub(&r.Z, &t0, &r.T) + FeAdd(&r.T, &t0, &r.T) +} + +func slide(r *[256]int8, a *[32]byte) { + for i := range r { + r[i] = int8(1 & (a[i>>3] >> uint(i&7))) + } + + for i := range r { + if r[i] != 0 { + for b := 1; b <= 6 && i+b < 256; b++ { + if r[i+b] != 0 { + if r[i]+(r[i+b]<= -15 { + r[i] -= r[i+b] << uint(b) + for k := i + b; k < 256; k++ { + if r[k] == 0 { + r[k] = 1 + break + } + r[k] = 0 + } + } else { + break + } + } + } + } + } +} + +// GeDoubleScalarMultVartime sets r = a*A + b*B +// where a = a[0]+256*a[1]+...+256^31 a[31]. +// and b = b[0]+256*b[1]+...+256^31 b[31]. +// B is the Ed25519 base point (x,4/5) with x positive. +func GeDoubleScalarMultVartime(r *ProjectiveGroupElement, a *[32]byte, A *ExtendedGroupElement, b *[32]byte) { + var aSlide, bSlide [256]int8 + var Ai [8]CachedGroupElement // A,3A,5A,7A,9A,11A,13A,15A + var t CompletedGroupElement + var u, A2 ExtendedGroupElement + var i int + + slide(&aSlide, a) + slide(&bSlide, b) + + A.ToCached(&Ai[0]) + A.Double(&t) + t.ToExtended(&A2) + + for i := 0; i < 7; i++ { + geAdd(&t, &A2, &Ai[i]) + t.ToExtended(&u) + u.ToCached(&Ai[i+1]) + } + + r.Zero() + + for i = 255; i >= 0; i-- { + if aSlide[i] != 0 || bSlide[i] != 0 { + break + } + } + + for ; i >= 0; i-- { + r.Double(&t) + + if aSlide[i] > 0 { + t.ToExtended(&u) + geAdd(&t, &u, &Ai[aSlide[i]/2]) + } else if aSlide[i] < 0 { + t.ToExtended(&u) + geSub(&t, &u, &Ai[(-aSlide[i])/2]) + } + + if bSlide[i] > 0 { + t.ToExtended(&u) + geMixedAdd(&t, &u, &bi[bSlide[i]/2]) + } else if bSlide[i] < 0 { + t.ToExtended(&u) + geMixedSub(&t, &u, &bi[(-bSlide[i])/2]) + } + + t.ToProjective(r) + } +} + +// equal returns 1 if b == c and 0 otherwise, assuming that b and c are +// non-negative. +func equal(b, c int32) int32 { + x := uint32(b ^ c) + x-- + return int32(x >> 31) +} + +// negative returns 1 if b < 0 and 0 otherwise. +func negative(b int32) int32 { + return (b >> 31) & 1 +} + +func PreComputedGroupElementCMove(t, u *PreComputedGroupElement, b int32) { + FeCMove(&t.yPlusX, &u.yPlusX, b) + FeCMove(&t.yMinusX, &u.yMinusX, b) + FeCMove(&t.xy2d, &u.xy2d, b) +} + +func selectPoint(t *PreComputedGroupElement, pos int32, b int32) { + var minusT PreComputedGroupElement + bNegative := negative(b) + bAbs := b - (((-bNegative) & b) << 1) + + t.Zero() + for i := int32(0); i < 8; i++ { + PreComputedGroupElementCMove(t, &base[pos][i], equal(bAbs, i+1)) + } + FeCopy(&minusT.yPlusX, &t.yMinusX) + FeCopy(&minusT.yMinusX, &t.yPlusX) + FeNeg(&minusT.xy2d, &t.xy2d) + PreComputedGroupElementCMove(t, &minusT, bNegative) +} + +// GeScalarMultBase computes h = a*B, where +// a = a[0]+256*a[1]+...+256^31 a[31] +// B is the Ed25519 base point (x,4/5) with x positive. +// +// Preconditions: +// a[31] <= 127 +func GeScalarMultBase(h *ExtendedGroupElement, a *[32]byte) { + var e [64]int8 + + for i, v := range a { + e[2*i] = int8(v & 15) + e[2*i+1] = int8((v >> 4) & 15) + } + + // each e[i] is between 0 and 15 and e[63] is between 0 and 7. + + carry := int8(0) + for i := 0; i < 63; i++ { + e[i] += carry + carry = (e[i] + 8) >> 4 + e[i] -= carry << 4 + } + e[63] += carry + // each e[i] is between -8 and 8. + + h.Zero() + var t PreComputedGroupElement + var r CompletedGroupElement + for i := int32(1); i < 64; i += 2 { + selectPoint(&t, i/2, int32(e[i])) + geMixedAdd(&r, h, &t) + r.ToExtended(h) + } + + var s ProjectiveGroupElement + + h.Double(&r) + r.ToProjective(&s) + s.Double(&r) + r.ToProjective(&s) + s.Double(&r) + r.ToProjective(&s) + s.Double(&r) + r.ToExtended(h) + + for i := int32(0); i < 64; i += 2 { + selectPoint(&t, i/2, int32(e[i])) + geMixedAdd(&r, h, &t) + r.ToExtended(h) + } +} + +// The scalars are GF(2^252 + 27742317777372353535851937790883648493). + +// Input: +// a[0]+256*a[1]+...+256^31*a[31] = a +// b[0]+256*b[1]+...+256^31*b[31] = b +// c[0]+256*c[1]+...+256^31*c[31] = c +// +// Output: +// s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l +// where l = 2^252 + 27742317777372353535851937790883648493. +func ScMulAdd(s, a, b, c *[32]byte) { + a0 := 2097151 & load3(a[:]) + a1 := 2097151 & (load4(a[2:]) >> 5) + a2 := 2097151 & (load3(a[5:]) >> 2) + a3 := 2097151 & (load4(a[7:]) >> 7) + a4 := 2097151 & (load4(a[10:]) >> 4) + a5 := 2097151 & (load3(a[13:]) >> 1) + a6 := 2097151 & (load4(a[15:]) >> 6) + a7 := 2097151 & (load3(a[18:]) >> 3) + a8 := 2097151 & load3(a[21:]) + a9 := 2097151 & (load4(a[23:]) >> 5) + a10 := 2097151 & (load3(a[26:]) >> 2) + a11 := (load4(a[28:]) >> 7) + b0 := 2097151 & load3(b[:]) + b1 := 2097151 & (load4(b[2:]) >> 5) + b2 := 2097151 & (load3(b[5:]) >> 2) + b3 := 2097151 & (load4(b[7:]) >> 7) + b4 := 2097151 & (load4(b[10:]) >> 4) + b5 := 2097151 & (load3(b[13:]) >> 1) + b6 := 2097151 & (load4(b[15:]) >> 6) + b7 := 2097151 & (load3(b[18:]) >> 3) + b8 := 2097151 & load3(b[21:]) + b9 := 2097151 & (load4(b[23:]) >> 5) + b10 := 2097151 & (load3(b[26:]) >> 2) + b11 := (load4(b[28:]) >> 7) + c0 := 2097151 & load3(c[:]) + c1 := 2097151 & (load4(c[2:]) >> 5) + c2 := 2097151 & (load3(c[5:]) >> 2) + c3 := 2097151 & (load4(c[7:]) >> 7) + c4 := 2097151 & (load4(c[10:]) >> 4) + c5 := 2097151 & (load3(c[13:]) >> 1) + c6 := 2097151 & (load4(c[15:]) >> 6) + c7 := 2097151 & (load3(c[18:]) >> 3) + c8 := 2097151 & load3(c[21:]) + c9 := 2097151 & (load4(c[23:]) >> 5) + c10 := 2097151 & (load3(c[26:]) >> 2) + c11 := (load4(c[28:]) >> 7) + var carry [23]int64 + + s0 := c0 + a0*b0 + s1 := c1 + a0*b1 + a1*b0 + s2 := c2 + a0*b2 + a1*b1 + a2*b0 + s3 := c3 + a0*b3 + a1*b2 + a2*b1 + a3*b0 + s4 := c4 + a0*b4 + a1*b3 + a2*b2 + a3*b1 + a4*b0 + s5 := c5 + a0*b5 + a1*b4 + a2*b3 + a3*b2 + a4*b1 + a5*b0 + s6 := c6 + a0*b6 + a1*b5 + a2*b4 + a3*b3 + a4*b2 + a5*b1 + a6*b0 + s7 := c7 + a0*b7 + a1*b6 + a2*b5 + a3*b4 + a4*b3 + a5*b2 + a6*b1 + a7*b0 + s8 := c8 + a0*b8 + a1*b7 + a2*b6 + a3*b5 + a4*b4 + a5*b3 + a6*b2 + a7*b1 + a8*b0 + s9 := c9 + a0*b9 + a1*b8 + a2*b7 + a3*b6 + a4*b5 + a5*b4 + a6*b3 + a7*b2 + a8*b1 + a9*b0 + s10 := c10 + a0*b10 + a1*b9 + a2*b8 + a3*b7 + a4*b6 + a5*b5 + a6*b4 + a7*b3 + a8*b2 + a9*b1 + a10*b0 + s11 := c11 + a0*b11 + a1*b10 + a2*b9 + a3*b8 + a4*b7 + a5*b6 + a6*b5 + a7*b4 + a8*b3 + a9*b2 + a10*b1 + a11*b0 + s12 := a1*b11 + a2*b10 + a3*b9 + a4*b8 + a5*b7 + a6*b6 + a7*b5 + a8*b4 + a9*b3 + a10*b2 + a11*b1 + s13 := a2*b11 + a3*b10 + a4*b9 + a5*b8 + a6*b7 + a7*b6 + a8*b5 + a9*b4 + a10*b3 + a11*b2 + s14 := a3*b11 + a4*b10 + a5*b9 + a6*b8 + a7*b7 + a8*b6 + a9*b5 + a10*b4 + a11*b3 + s15 := a4*b11 + a5*b10 + a6*b9 + a7*b8 + a8*b7 + a9*b6 + a10*b5 + a11*b4 + s16 := a5*b11 + a6*b10 + a7*b9 + a8*b8 + a9*b7 + a10*b6 + a11*b5 + s17 := a6*b11 + a7*b10 + a8*b9 + a9*b8 + a10*b7 + a11*b6 + s18 := a7*b11 + a8*b10 + a9*b9 + a10*b8 + a11*b7 + s19 := a8*b11 + a9*b10 + a10*b9 + a11*b8 + s20 := a9*b11 + a10*b10 + a11*b9 + s21 := a10*b11 + a11*b10 + s22 := a11 * b11 + s23 := int64(0) + + carry[0] = (s0 + (1 << 20)) >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[2] = (s2 + (1 << 20)) >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[4] = (s4 + (1 << 20)) >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[6] = (s6 + (1 << 20)) >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[8] = (s8 + (1 << 20)) >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[10] = (s10 + (1 << 20)) >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + carry[12] = (s12 + (1 << 20)) >> 21 + s13 += carry[12] + s12 -= carry[12] << 21 + carry[14] = (s14 + (1 << 20)) >> 21 + s15 += carry[14] + s14 -= carry[14] << 21 + carry[16] = (s16 + (1 << 20)) >> 21 + s17 += carry[16] + s16 -= carry[16] << 21 + carry[18] = (s18 + (1 << 20)) >> 21 + s19 += carry[18] + s18 -= carry[18] << 21 + carry[20] = (s20 + (1 << 20)) >> 21 + s21 += carry[20] + s20 -= carry[20] << 21 + carry[22] = (s22 + (1 << 20)) >> 21 + s23 += carry[22] + s22 -= carry[22] << 21 + + carry[1] = (s1 + (1 << 20)) >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[3] = (s3 + (1 << 20)) >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[5] = (s5 + (1 << 20)) >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[7] = (s7 + (1 << 20)) >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[9] = (s9 + (1 << 20)) >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[11] = (s11 + (1 << 20)) >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + carry[13] = (s13 + (1 << 20)) >> 21 + s14 += carry[13] + s13 -= carry[13] << 21 + carry[15] = (s15 + (1 << 20)) >> 21 + s16 += carry[15] + s15 -= carry[15] << 21 + carry[17] = (s17 + (1 << 20)) >> 21 + s18 += carry[17] + s17 -= carry[17] << 21 + carry[19] = (s19 + (1 << 20)) >> 21 + s20 += carry[19] + s19 -= carry[19] << 21 + carry[21] = (s21 + (1 << 20)) >> 21 + s22 += carry[21] + s21 -= carry[21] << 21 + + s11 += s23 * 666643 + s12 += s23 * 470296 + s13 += s23 * 654183 + s14 -= s23 * 997805 + s15 += s23 * 136657 + s16 -= s23 * 683901 + s23 = 0 + + s10 += s22 * 666643 + s11 += s22 * 470296 + s12 += s22 * 654183 + s13 -= s22 * 997805 + s14 += s22 * 136657 + s15 -= s22 * 683901 + s22 = 0 + + s9 += s21 * 666643 + s10 += s21 * 470296 + s11 += s21 * 654183 + s12 -= s21 * 997805 + s13 += s21 * 136657 + s14 -= s21 * 683901 + s21 = 0 + + s8 += s20 * 666643 + s9 += s20 * 470296 + s10 += s20 * 654183 + s11 -= s20 * 997805 + s12 += s20 * 136657 + s13 -= s20 * 683901 + s20 = 0 + + s7 += s19 * 666643 + s8 += s19 * 470296 + s9 += s19 * 654183 + s10 -= s19 * 997805 + s11 += s19 * 136657 + s12 -= s19 * 683901 + s19 = 0 + + s6 += s18 * 666643 + s7 += s18 * 470296 + s8 += s18 * 654183 + s9 -= s18 * 997805 + s10 += s18 * 136657 + s11 -= s18 * 683901 + s18 = 0 + + carry[6] = (s6 + (1 << 20)) >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[8] = (s8 + (1 << 20)) >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[10] = (s10 + (1 << 20)) >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + carry[12] = (s12 + (1 << 20)) >> 21 + s13 += carry[12] + s12 -= carry[12] << 21 + carry[14] = (s14 + (1 << 20)) >> 21 + s15 += carry[14] + s14 -= carry[14] << 21 + carry[16] = (s16 + (1 << 20)) >> 21 + s17 += carry[16] + s16 -= carry[16] << 21 + + carry[7] = (s7 + (1 << 20)) >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[9] = (s9 + (1 << 20)) >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[11] = (s11 + (1 << 20)) >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + carry[13] = (s13 + (1 << 20)) >> 21 + s14 += carry[13] + s13 -= carry[13] << 21 + carry[15] = (s15 + (1 << 20)) >> 21 + s16 += carry[15] + s15 -= carry[15] << 21 + + s5 += s17 * 666643 + s6 += s17 * 470296 + s7 += s17 * 654183 + s8 -= s17 * 997805 + s9 += s17 * 136657 + s10 -= s17 * 683901 + s17 = 0 + + s4 += s16 * 666643 + s5 += s16 * 470296 + s6 += s16 * 654183 + s7 -= s16 * 997805 + s8 += s16 * 136657 + s9 -= s16 * 683901 + s16 = 0 + + s3 += s15 * 666643 + s4 += s15 * 470296 + s5 += s15 * 654183 + s6 -= s15 * 997805 + s7 += s15 * 136657 + s8 -= s15 * 683901 + s15 = 0 + + s2 += s14 * 666643 + s3 += s14 * 470296 + s4 += s14 * 654183 + s5 -= s14 * 997805 + s6 += s14 * 136657 + s7 -= s14 * 683901 + s14 = 0 + + s1 += s13 * 666643 + s2 += s13 * 470296 + s3 += s13 * 654183 + s4 -= s13 * 997805 + s5 += s13 * 136657 + s6 -= s13 * 683901 + s13 = 0 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = (s0 + (1 << 20)) >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[2] = (s2 + (1 << 20)) >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[4] = (s4 + (1 << 20)) >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[6] = (s6 + (1 << 20)) >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[8] = (s8 + (1 << 20)) >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[10] = (s10 + (1 << 20)) >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + + carry[1] = (s1 + (1 << 20)) >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[3] = (s3 + (1 << 20)) >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[5] = (s5 + (1 << 20)) >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[7] = (s7 + (1 << 20)) >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[9] = (s9 + (1 << 20)) >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[11] = (s11 + (1 << 20)) >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = s0 >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[1] = s1 >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[2] = s2 >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[3] = s3 >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[4] = s4 >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[5] = s5 >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[6] = s6 >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[7] = s7 >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[8] = s8 >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[9] = s9 >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[10] = s10 >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + carry[11] = s11 >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = s0 >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[1] = s1 >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[2] = s2 >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[3] = s3 >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[4] = s4 >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[5] = s5 >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[6] = s6 >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[7] = s7 >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[8] = s8 >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[9] = s9 >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[10] = s10 >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + + s[0] = byte(s0 >> 0) + s[1] = byte(s0 >> 8) + s[2] = byte((s0 >> 16) | (s1 << 5)) + s[3] = byte(s1 >> 3) + s[4] = byte(s1 >> 11) + s[5] = byte((s1 >> 19) | (s2 << 2)) + s[6] = byte(s2 >> 6) + s[7] = byte((s2 >> 14) | (s3 << 7)) + s[8] = byte(s3 >> 1) + s[9] = byte(s3 >> 9) + s[10] = byte((s3 >> 17) | (s4 << 4)) + s[11] = byte(s4 >> 4) + s[12] = byte(s4 >> 12) + s[13] = byte((s4 >> 20) | (s5 << 1)) + s[14] = byte(s5 >> 7) + s[15] = byte((s5 >> 15) | (s6 << 6)) + s[16] = byte(s6 >> 2) + s[17] = byte(s6 >> 10) + s[18] = byte((s6 >> 18) | (s7 << 3)) + s[19] = byte(s7 >> 5) + s[20] = byte(s7 >> 13) + s[21] = byte(s8 >> 0) + s[22] = byte(s8 >> 8) + s[23] = byte((s8 >> 16) | (s9 << 5)) + s[24] = byte(s9 >> 3) + s[25] = byte(s9 >> 11) + s[26] = byte((s9 >> 19) | (s10 << 2)) + s[27] = byte(s10 >> 6) + s[28] = byte((s10 >> 14) | (s11 << 7)) + s[29] = byte(s11 >> 1) + s[30] = byte(s11 >> 9) + s[31] = byte(s11 >> 17) +} + +// Input: +// s[0]+256*s[1]+...+256^63*s[63] = s +// +// Output: +// s[0]+256*s[1]+...+256^31*s[31] = s mod l +// where l = 2^252 + 27742317777372353535851937790883648493. +func ScReduce(out *[32]byte, s *[64]byte) { + s0 := 2097151 & load3(s[:]) + s1 := 2097151 & (load4(s[2:]) >> 5) + s2 := 2097151 & (load3(s[5:]) >> 2) + s3 := 2097151 & (load4(s[7:]) >> 7) + s4 := 2097151 & (load4(s[10:]) >> 4) + s5 := 2097151 & (load3(s[13:]) >> 1) + s6 := 2097151 & (load4(s[15:]) >> 6) + s7 := 2097151 & (load3(s[18:]) >> 3) + s8 := 2097151 & load3(s[21:]) + s9 := 2097151 & (load4(s[23:]) >> 5) + s10 := 2097151 & (load3(s[26:]) >> 2) + s11 := 2097151 & (load4(s[28:]) >> 7) + s12 := 2097151 & (load4(s[31:]) >> 4) + s13 := 2097151 & (load3(s[34:]) >> 1) + s14 := 2097151 & (load4(s[36:]) >> 6) + s15 := 2097151 & (load3(s[39:]) >> 3) + s16 := 2097151 & load3(s[42:]) + s17 := 2097151 & (load4(s[44:]) >> 5) + s18 := 2097151 & (load3(s[47:]) >> 2) + s19 := 2097151 & (load4(s[49:]) >> 7) + s20 := 2097151 & (load4(s[52:]) >> 4) + s21 := 2097151 & (load3(s[55:]) >> 1) + s22 := 2097151 & (load4(s[57:]) >> 6) + s23 := (load4(s[60:]) >> 3) + + s11 += s23 * 666643 + s12 += s23 * 470296 + s13 += s23 * 654183 + s14 -= s23 * 997805 + s15 += s23 * 136657 + s16 -= s23 * 683901 + s23 = 0 + + s10 += s22 * 666643 + s11 += s22 * 470296 + s12 += s22 * 654183 + s13 -= s22 * 997805 + s14 += s22 * 136657 + s15 -= s22 * 683901 + s22 = 0 + + s9 += s21 * 666643 + s10 += s21 * 470296 + s11 += s21 * 654183 + s12 -= s21 * 997805 + s13 += s21 * 136657 + s14 -= s21 * 683901 + s21 = 0 + + s8 += s20 * 666643 + s9 += s20 * 470296 + s10 += s20 * 654183 + s11 -= s20 * 997805 + s12 += s20 * 136657 + s13 -= s20 * 683901 + s20 = 0 + + s7 += s19 * 666643 + s8 += s19 * 470296 + s9 += s19 * 654183 + s10 -= s19 * 997805 + s11 += s19 * 136657 + s12 -= s19 * 683901 + s19 = 0 + + s6 += s18 * 666643 + s7 += s18 * 470296 + s8 += s18 * 654183 + s9 -= s18 * 997805 + s10 += s18 * 136657 + s11 -= s18 * 683901 + s18 = 0 + + var carry [17]int64 + + carry[6] = (s6 + (1 << 20)) >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[8] = (s8 + (1 << 20)) >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[10] = (s10 + (1 << 20)) >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + carry[12] = (s12 + (1 << 20)) >> 21 + s13 += carry[12] + s12 -= carry[12] << 21 + carry[14] = (s14 + (1 << 20)) >> 21 + s15 += carry[14] + s14 -= carry[14] << 21 + carry[16] = (s16 + (1 << 20)) >> 21 + s17 += carry[16] + s16 -= carry[16] << 21 + + carry[7] = (s7 + (1 << 20)) >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[9] = (s9 + (1 << 20)) >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[11] = (s11 + (1 << 20)) >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + carry[13] = (s13 + (1 << 20)) >> 21 + s14 += carry[13] + s13 -= carry[13] << 21 + carry[15] = (s15 + (1 << 20)) >> 21 + s16 += carry[15] + s15 -= carry[15] << 21 + + s5 += s17 * 666643 + s6 += s17 * 470296 + s7 += s17 * 654183 + s8 -= s17 * 997805 + s9 += s17 * 136657 + s10 -= s17 * 683901 + s17 = 0 + + s4 += s16 * 666643 + s5 += s16 * 470296 + s6 += s16 * 654183 + s7 -= s16 * 997805 + s8 += s16 * 136657 + s9 -= s16 * 683901 + s16 = 0 + + s3 += s15 * 666643 + s4 += s15 * 470296 + s5 += s15 * 654183 + s6 -= s15 * 997805 + s7 += s15 * 136657 + s8 -= s15 * 683901 + s15 = 0 + + s2 += s14 * 666643 + s3 += s14 * 470296 + s4 += s14 * 654183 + s5 -= s14 * 997805 + s6 += s14 * 136657 + s7 -= s14 * 683901 + s14 = 0 + + s1 += s13 * 666643 + s2 += s13 * 470296 + s3 += s13 * 654183 + s4 -= s13 * 997805 + s5 += s13 * 136657 + s6 -= s13 * 683901 + s13 = 0 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = (s0 + (1 << 20)) >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[2] = (s2 + (1 << 20)) >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[4] = (s4 + (1 << 20)) >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[6] = (s6 + (1 << 20)) >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[8] = (s8 + (1 << 20)) >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[10] = (s10 + (1 << 20)) >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + + carry[1] = (s1 + (1 << 20)) >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[3] = (s3 + (1 << 20)) >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[5] = (s5 + (1 << 20)) >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[7] = (s7 + (1 << 20)) >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[9] = (s9 + (1 << 20)) >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[11] = (s11 + (1 << 20)) >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = s0 >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[1] = s1 >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[2] = s2 >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[3] = s3 >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[4] = s4 >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[5] = s5 >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[6] = s6 >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[7] = s7 >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[8] = s8 >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[9] = s9 >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[10] = s10 >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + carry[11] = s11 >> 21 + s12 += carry[11] + s11 -= carry[11] << 21 + + s0 += s12 * 666643 + s1 += s12 * 470296 + s2 += s12 * 654183 + s3 -= s12 * 997805 + s4 += s12 * 136657 + s5 -= s12 * 683901 + s12 = 0 + + carry[0] = s0 >> 21 + s1 += carry[0] + s0 -= carry[0] << 21 + carry[1] = s1 >> 21 + s2 += carry[1] + s1 -= carry[1] << 21 + carry[2] = s2 >> 21 + s3 += carry[2] + s2 -= carry[2] << 21 + carry[3] = s3 >> 21 + s4 += carry[3] + s3 -= carry[3] << 21 + carry[4] = s4 >> 21 + s5 += carry[4] + s4 -= carry[4] << 21 + carry[5] = s5 >> 21 + s6 += carry[5] + s5 -= carry[5] << 21 + carry[6] = s6 >> 21 + s7 += carry[6] + s6 -= carry[6] << 21 + carry[7] = s7 >> 21 + s8 += carry[7] + s7 -= carry[7] << 21 + carry[8] = s8 >> 21 + s9 += carry[8] + s8 -= carry[8] << 21 + carry[9] = s9 >> 21 + s10 += carry[9] + s9 -= carry[9] << 21 + carry[10] = s10 >> 21 + s11 += carry[10] + s10 -= carry[10] << 21 + + out[0] = byte(s0 >> 0) + out[1] = byte(s0 >> 8) + out[2] = byte((s0 >> 16) | (s1 << 5)) + out[3] = byte(s1 >> 3) + out[4] = byte(s1 >> 11) + out[5] = byte((s1 >> 19) | (s2 << 2)) + out[6] = byte(s2 >> 6) + out[7] = byte((s2 >> 14) | (s3 << 7)) + out[8] = byte(s3 >> 1) + out[9] = byte(s3 >> 9) + out[10] = byte((s3 >> 17) | (s4 << 4)) + out[11] = byte(s4 >> 4) + out[12] = byte(s4 >> 12) + out[13] = byte((s4 >> 20) | (s5 << 1)) + out[14] = byte(s5 >> 7) + out[15] = byte((s5 >> 15) | (s6 << 6)) + out[16] = byte(s6 >> 2) + out[17] = byte(s6 >> 10) + out[18] = byte((s6 >> 18) | (s7 << 3)) + out[19] = byte(s7 >> 5) + out[20] = byte(s7 >> 13) + out[21] = byte(s8 >> 0) + out[22] = byte(s8 >> 8) + out[23] = byte((s8 >> 16) | (s9 << 5)) + out[24] = byte(s9 >> 3) + out[25] = byte(s9 >> 11) + out[26] = byte((s9 >> 19) | (s10 << 2)) + out[27] = byte(s10 >> 6) + out[28] = byte((s10 >> 14) | (s11 << 7)) + out[29] = byte(s11 >> 1) + out[30] = byte(s11 >> 9) + out[31] = byte(s11 >> 17) +} + +// order is the order of Curve25519 in little-endian form. +var order = [4]uint64{0x5812631a5cf5d3ed, 0x14def9dea2f79cd6, 0, 0x1000000000000000} + +// ScMinimal returns true if the given scalar is less than the order of the +// curve. +func ScMinimal(scalar *[32]byte) bool { + for i := 3; ; i-- { + v := binary.LittleEndian.Uint64(scalar[i*8:]) + if v > order[i] { + return false + } else if v < order[i] { + break + } else if i == 0 { + return false + } + } + + return true +} diff --git a/vendor/gopkg.in/square/go-jose.v2/LICENSE b/vendor/gopkg.in/square/go-jose.v2/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/gopkg.in/square/go-jose.v2/asymmetric.go b/vendor/gopkg.in/square/go-jose.v2/asymmetric.go new file mode 100644 index 00000000000..5272648faa9 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/asymmetric.go @@ -0,0 +1,592 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "crypto" + "crypto/aes" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "errors" + "fmt" + "math/big" + + "golang.org/x/crypto/ed25519" + "gopkg.in/square/go-jose.v2/cipher" + "gopkg.in/square/go-jose.v2/json" +) + +// A generic RSA-based encrypter/verifier +type rsaEncrypterVerifier struct { + publicKey *rsa.PublicKey +} + +// A generic RSA-based decrypter/signer +type rsaDecrypterSigner struct { + privateKey *rsa.PrivateKey +} + +// A generic EC-based encrypter/verifier +type ecEncrypterVerifier struct { + publicKey *ecdsa.PublicKey +} + +type edEncrypterVerifier struct { + publicKey ed25519.PublicKey +} + +// A key generator for ECDH-ES +type ecKeyGenerator struct { + size int + algID string + publicKey *ecdsa.PublicKey +} + +// A generic EC-based decrypter/signer +type ecDecrypterSigner struct { + privateKey *ecdsa.PrivateKey +} + +type edDecrypterSigner struct { + privateKey ed25519.PrivateKey +} + +// newRSARecipient creates recipientKeyInfo based on the given key. +func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case RSA1_5, RSA_OAEP, RSA_OAEP_256: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &rsaEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newRSASigner creates a recipientSigInfo based on the given key. +func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case RS256, RS384, RS512, PS256, PS384, PS512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &rsaDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { + if sigAlg != EdDSA { + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &edDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// newECDHRecipient creates recipientKeyInfo based on the given key. +func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &ecEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newECDSASigner creates a recipientSigInfo based on the given key. +func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case ES256, ES384, ES512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &ecDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// Encrypt the given payload and update the object. +func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + encryptedKey, err := ctx.encrypt(cek, alg) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: encryptedKey, + header: &rawHeader{}, + }, nil +} + +// Encrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { + switch alg { + case RSA1_5: + return rsa.EncryptPKCS1v15(randReader, ctx.publicKey, cek) + case RSA_OAEP: + return rsa.EncryptOAEP(sha1.New(), randReader, ctx.publicKey, cek, []byte{}) + case RSA_OAEP_256: + return rsa.EncryptOAEP(sha256.New(), randReader, ctx.publicKey, cek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Decrypt the given payload and return the content encryption key. +func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) +} + +// Decrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { + // Note: The random reader on decrypt operations is only used for blinding, + // so stubbing is meanlingless (hence the direct use of rand.Reader). + switch alg { + case RSA1_5: + defer func() { + // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload + // because of an index out of bounds error, which we want to ignore. + // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() + // only exists for preventing crashes with unpatched versions. + // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k + // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 + _ = recover() + }() + + // Perform some input validation. + keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 + if keyBytes != len(jek) { + // Input size is incorrect, the encrypted payload should always match + // the size of the public modulus (e.g. using a 2048 bit key will + // produce 256 bytes of output). Reject this since it's invalid input. + return nil, ErrCryptoFailure + } + + cek, _, err := generator.genKey() + if err != nil { + return nil, ErrCryptoFailure + } + + // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to + // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing + // the Million Message Attack on Cryptographic Message Syntax". We are + // therefore deliberately ignoring errors here. + _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) + + return cek, nil + case RSA_OAEP: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + case RSA_OAEP_256: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Sign the given payload +func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return Signature{}, ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + var out []byte + var err error + + switch alg { + case RS256, RS384, RS512: + out, err = rsa.SignPKCS1v15(randReader, ctx.privateKey, hash, hashed) + case PS256, PS384, PS512: + out, err = rsa.SignPSS(randReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }) + } + + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + switch alg { + case RS256, RS384, RS512: + return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) + case PS256, PS384, PS512: + return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) + } + + return ErrUnsupportedAlgorithm +} + +// Encrypt the given payload and update the object. +func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + switch alg { + case ECDH_ES: + // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. + return recipientInfo{ + header: &rawHeader{}, + }, nil + case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientInfo{}, ErrUnsupportedAlgorithm + } + + generator := ecKeyGenerator{ + algID: string(alg), + publicKey: ctx.publicKey, + } + + switch alg { + case ECDH_ES_A128KW: + generator.size = 16 + case ECDH_ES_A192KW: + generator.size = 24 + case ECDH_ES_A256KW: + generator.size = 32 + } + + kek, header, err := generator.genKey() + if err != nil { + return recipientInfo{}, err + } + + block, err := aes.NewCipher(kek) + if err != nil { + return recipientInfo{}, err + } + + jek, err := josecipher.KeyWrap(block, cek) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: jek, + header: &header, + }, nil +} + +// Get key size for EC key generator +func (ctx ecKeyGenerator) keySize() int { + return ctx.size +} + +// Get a content encryption key for ECDH-ES +func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { + priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, randReader) + if err != nil { + return nil, rawHeader{}, err + } + + out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) + + b, err := json.Marshal(&JSONWebKey{ + Key: &priv.PublicKey, + }) + if err != nil { + return nil, nil, err + } + + headers := rawHeader{ + headerEPK: makeRawMessage(b), + } + + return out, headers, nil +} + +// Decrypt the given payload and return the content encryption key. +func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + epk, err := headers.getEPK() + if err != nil { + return nil, errors.New("square/go-jose: invalid epk header") + } + if epk == nil { + return nil, errors.New("square/go-jose: missing epk header") + } + + publicKey, ok := epk.Key.(*ecdsa.PublicKey) + if publicKey == nil || !ok { + return nil, errors.New("square/go-jose: invalid epk header") + } + + if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return nil, errors.New("square/go-jose: invalid public key in epk header") + } + + apuData, err := headers.getAPU() + if err != nil { + return nil, errors.New("square/go-jose: invalid apu header") + } + apvData, err := headers.getAPV() + if err != nil { + return nil, errors.New("square/go-jose: invalid apv header") + } + + deriveKey := func(algID string, size int) []byte { + return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) + } + + var keySize int + + algorithm := headers.getAlgorithm() + switch algorithm { + case ECDH_ES: + // ECDH-ES uses direct key agreement, no key unwrapping necessary. + return deriveKey(string(headers.getEncryption()), generator.keySize()), nil + case ECDH_ES_A128KW: + keySize = 16 + case ECDH_ES_A192KW: + keySize = 24 + case ECDH_ES_A256KW: + keySize = 32 + default: + return nil, ErrUnsupportedAlgorithm + } + + key := deriveKey(string(algorithm), keySize) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + return josecipher.KeyUnwrap(block, recipient.encryptedKey) +} + +func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + if alg != EdDSA { + return Signature{}, ErrUnsupportedAlgorithm + } + + sig, err := ctx.privateKey.Sign(randReader, payload, crypto.Hash(0)) + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: sig, + protected: &rawHeader{}, + }, nil +} + +func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + if alg != EdDSA { + return ErrUnsupportedAlgorithm + } + ok := ed25519.Verify(ctx.publicKey, payload, signature) + if !ok { + return errors.New("square/go-jose: ed25519 signature failed to verify") + } + return nil +} + +// Sign the given payload +func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var expectedBitSize int + var hash crypto.Hash + + switch alg { + case ES256: + expectedBitSize = 256 + hash = crypto.SHA256 + case ES384: + expectedBitSize = 384 + hash = crypto.SHA384 + case ES512: + expectedBitSize = 521 + hash = crypto.SHA512 + } + + curveBits := ctx.privateKey.Curve.Params().BitSize + if expectedBitSize != curveBits { + return Signature{}, fmt.Errorf("square/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r, s, err := ecdsa.Sign(randReader, ctx.privateKey, hashed) + if err != nil { + return Signature{}, err + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + + // We serialize the outputs (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + out := append(rBytesPadded, sBytesPadded...) + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var keySize int + var hash crypto.Hash + + switch alg { + case ES256: + keySize = 32 + hash = crypto.SHA256 + case ES384: + keySize = 48 + hash = crypto.SHA384 + case ES512: + keySize = 66 + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + if len(signature) != 2*keySize { + return fmt.Errorf("square/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r := big.NewInt(0).SetBytes(signature[:keySize]) + s := big.NewInt(0).SetBytes(signature[keySize:]) + + match := ecdsa.Verify(ctx.publicKey, hashed, r, s) + if !match { + return errors.New("square/go-jose: ecdsa signature failed to verify") + } + + return nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go b/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go new file mode 100644 index 00000000000..126b85ce252 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go @@ -0,0 +1,196 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "bytes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/binary" + "errors" + "hash" +) + +const ( + nonceBytes = 16 +) + +// NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. +func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { + keySize := len(key) / 2 + integrityKey := key[:keySize] + encryptionKey := key[keySize:] + + blockCipher, err := newBlockCipher(encryptionKey) + if err != nil { + return nil, err + } + + var hash func() hash.Hash + switch keySize { + case 16: + hash = sha256.New + case 24: + hash = sha512.New384 + case 32: + hash = sha512.New + } + + return &cbcAEAD{ + hash: hash, + blockCipher: blockCipher, + authtagBytes: keySize, + integrityKey: integrityKey, + }, nil +} + +// An AEAD based on CBC+HMAC +type cbcAEAD struct { + hash func() hash.Hash + authtagBytes int + integrityKey []byte + blockCipher cipher.Block +} + +func (ctx *cbcAEAD) NonceSize() int { + return nonceBytes +} + +func (ctx *cbcAEAD) Overhead() int { + // Maximum overhead is block size (for padding) plus auth tag length, where + // the length of the auth tag is equivalent to the key size. + return ctx.blockCipher.BlockSize() + ctx.authtagBytes +} + +// Seal encrypts and authenticates the plaintext. +func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { + // Output buffer -- must take care not to mangle plaintext input. + ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] + copy(ciphertext, plaintext) + ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) + + cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) + + cbc.CryptBlocks(ciphertext, ciphertext) + authtag := ctx.computeAuthTag(data, nonce, ciphertext) + + ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) + copy(out, ciphertext) + copy(out[len(ciphertext):], authtag) + + return ret +} + +// Open decrypts and authenticates the ciphertext. +func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + if len(ciphertext) < ctx.authtagBytes { + return nil, errors.New("square/go-jose: invalid ciphertext (too short)") + } + + offset := len(ciphertext) - ctx.authtagBytes + expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) + match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) + if match != 1 { + return nil, errors.New("square/go-jose: invalid ciphertext (auth tag mismatch)") + } + + cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) + + // Make copy of ciphertext buffer, don't want to modify in place + buffer := append([]byte{}, []byte(ciphertext[:offset])...) + + if len(buffer)%ctx.blockCipher.BlockSize() > 0 { + return nil, errors.New("square/go-jose: invalid ciphertext (invalid length)") + } + + cbc.CryptBlocks(buffer, buffer) + + // Remove padding + plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) + if err != nil { + return nil, err + } + + ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) + copy(out, plaintext) + + return ret, nil +} + +// Compute an authentication tag +func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { + buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) + n := 0 + n += copy(buffer, aad) + n += copy(buffer[n:], nonce) + n += copy(buffer[n:], ciphertext) + binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) + + // According to documentation, Write() on hash.Hash never fails. + hmac := hmac.New(ctx.hash, ctx.integrityKey) + _, _ = hmac.Write(buffer) + + return hmac.Sum(nil)[:ctx.authtagBytes] +} + +// resize ensures the the given slice has a capacity of at least n bytes. +// If the capacity of the slice is less than n, a new slice is allocated +// and the existing data will be copied. +func resize(in []byte, n uint64) (head, tail []byte) { + if uint64(cap(in)) >= n { + head = in[:n] + } else { + head = make([]byte, n) + copy(head, in) + } + + tail = head[len(in):] + return +} + +// Apply padding +func padBuffer(buffer []byte, blockSize int) []byte { + missing := blockSize - (len(buffer) % blockSize) + ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) + padding := bytes.Repeat([]byte{byte(missing)}, missing) + copy(out, padding) + return ret +} + +// Remove padding +func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { + if len(buffer)%blockSize != 0 { + return nil, errors.New("square/go-jose: invalid padding") + } + + last := buffer[len(buffer)-1] + count := int(last) + + if count == 0 || count > blockSize || count > len(buffer) { + return nil, errors.New("square/go-jose: invalid padding") + } + + padding := bytes.Repeat([]byte{last}, count) + if !bytes.HasSuffix(buffer, padding) { + return nil, errors.New("square/go-jose: invalid padding") + } + + return buffer[:len(buffer)-count], nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go b/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go new file mode 100644 index 00000000000..f62c3bdba5d --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go @@ -0,0 +1,75 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto" + "encoding/binary" + "hash" + "io" +) + +type concatKDF struct { + z, info []byte + i uint32 + cache []byte + hasher hash.Hash +} + +// NewConcatKDF builds a KDF reader based on the given inputs. +func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { + buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) + n := 0 + n += copy(buffer, algID) + n += copy(buffer[n:], ptyUInfo) + n += copy(buffer[n:], ptyVInfo) + n += copy(buffer[n:], supPubInfo) + copy(buffer[n:], supPrivInfo) + + hasher := hash.New() + + return &concatKDF{ + z: z, + info: buffer, + hasher: hasher, + cache: []byte{}, + i: 1, + } +} + +func (ctx *concatKDF) Read(out []byte) (int, error) { + copied := copy(out, ctx.cache) + ctx.cache = ctx.cache[copied:] + + for copied < len(out) { + ctx.hasher.Reset() + + // Write on a hash.Hash never fails + _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) + _, _ = ctx.hasher.Write(ctx.z) + _, _ = ctx.hasher.Write(ctx.info) + + hash := ctx.hasher.Sum(nil) + chunkCopied := copy(out[copied:], hash) + copied += chunkCopied + ctx.cache = hash[chunkCopied:] + + ctx.i++ + } + + return copied, nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go b/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go new file mode 100644 index 00000000000..c128e327f31 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go @@ -0,0 +1,62 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto" + "crypto/ecdsa" + "encoding/binary" +) + +// DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. +// It is an error to call this function with a private/public key that are not on the same +// curve. Callers must ensure that the keys are valid before calling this function. Output +// size may be at most 1<<16 bytes (64 KiB). +func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { + if size > 1<<16 { + panic("ECDH-ES output size too large, must be less than or equal to 1<<16") + } + + // algId, partyUInfo, partyVInfo inputs must be prefixed with the length + algID := lengthPrefixed([]byte(alg)) + ptyUInfo := lengthPrefixed(apuData) + ptyVInfo := lengthPrefixed(apvData) + + // suppPubInfo is the encoded length of the output size in bits + supPubInfo := make([]byte, 4) + binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) + + if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { + panic("public key not on same curve as private key") + } + + z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) + reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) + + key := make([]byte, size) + + // Read on the KDF will never fail + _, _ = reader.Read(key) + return key +} + +func lengthPrefixed(data []byte) []byte { + out := make([]byte, len(data)+4) + binary.BigEndian.PutUint32(out, uint32(len(data))) + copy(out[4:], data) + return out +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go b/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go new file mode 100644 index 00000000000..1d36d501510 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go @@ -0,0 +1,109 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto/cipher" + "crypto/subtle" + "encoding/binary" + "errors" +) + +var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} + +// KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. +func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { + if len(cek)%8 != 0 { + return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") + } + + n := len(cek) / 8 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], cek[i*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer, defaultIV) + + for t := 0; t < 6*n; t++ { + copy(buffer[8:], r[t%n]) + + block.Encrypt(buffer, buffer) + + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] = buffer[i] ^ tBytes[i] + } + copy(r[t%n], buffer[8:]) + } + + out := make([]byte, (n+1)*8) + copy(out, buffer[:8]) + for i := range r { + copy(out[(i+1)*8:], r[i]) + } + + return out, nil +} + +// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. +func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { + if len(ciphertext)%8 != 0 { + return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") + } + + n := (len(ciphertext) / 8) - 1 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], ciphertext[(i+1)*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer[:8], ciphertext[:8]) + + for t := 6*n - 1; t >= 0; t-- { + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] = buffer[i] ^ tBytes[i] + } + copy(buffer[8:], r[t%n]) + + block.Decrypt(buffer, buffer) + + copy(r[t%n], buffer[8:]) + } + + if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { + return nil, errors.New("square/go-jose: failed to unwrap key") + } + + out := make([]byte, n*8) + for i := range r { + copy(out[i*8:], r[i]) + } + + return out, nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/crypter.go b/vendor/gopkg.in/square/go-jose.v2/crypter.go new file mode 100644 index 00000000000..9ee44f8bd58 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/crypter.go @@ -0,0 +1,532 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "fmt" + "reflect" + + "gopkg.in/square/go-jose.v2/json" +) + +// Encrypter represents an encrypter which produces an encrypted JWE object. +type Encrypter interface { + Encrypt(plaintext []byte) (*JSONWebEncryption, error) + EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) + Options() EncrypterOptions +} + +// A generic content cipher +type contentCipher interface { + keySize() int + encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) + decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) +} + +// A key generator (for generating/getting a CEK) +type keyGenerator interface { + keySize() int + genKey() ([]byte, rawHeader, error) +} + +// A generic key encrypter +type keyEncrypter interface { + encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key +} + +// A generic key decrypter +type keyDecrypter interface { + decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key +} + +// A generic encrypter based on the given key encrypter and content cipher. +type genericEncrypter struct { + contentAlg ContentEncryption + compressionAlg CompressionAlgorithm + cipher contentCipher + recipients []recipientKeyInfo + keyGenerator keyGenerator + extraHeaders map[HeaderKey]interface{} +} + +type recipientKeyInfo struct { + keyID string + keyAlg KeyAlgorithm + keyEncrypter keyEncrypter +} + +// EncrypterOptions represents options that can be set on new encrypters. +type EncrypterOptions struct { + Compression CompressionAlgorithm + + // Optional map of additional keys to be inserted into the protected header + // of a JWS object. Some specifications which make use of JWS like to insert + // additional values here. All values must be JSON-serializable. + ExtraHeaders map[HeaderKey]interface{} +} + +// WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it +// if necessary. It returns itself and so can be used in a fluent style. +func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { + if eo.ExtraHeaders == nil { + eo.ExtraHeaders = map[HeaderKey]interface{}{} + } + eo.ExtraHeaders[k] = v + return eo +} + +// WithContentType adds a content type ("cty") header and returns the updated +// EncrypterOptions. +func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderContentType, contentType) +} + +// WithType adds a type ("typ") header and returns the updated EncrypterOptions. +func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderType, typ) +} + +// Recipient represents an algorithm/key to encrypt messages to. +// +// PBES2Count and PBES2Salt correspond with the "p2c" and "p2s" headers used +// on the password-based encryption algorithms PBES2-HS256+A128KW, +// PBES2-HS384+A192KW, and PBES2-HS512+A256KW. If they are not provided a safe +// default of 100000 will be used for the count and a 128-bit random salt will +// be generated. +type Recipient struct { + Algorithm KeyAlgorithm + Key interface{} + KeyID string + PBES2Count int + PBES2Salt []byte +} + +// NewEncrypter creates an appropriate encrypter based on the key type +func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: getContentCipher(enc), + } + if opts != nil { + encrypter.compressionAlg = opts.Compression + encrypter.extraHeaders = opts.ExtraHeaders + } + + if encrypter.cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + + var keyID string + var rawKey interface{} + switch encryptionKey := rcpt.Key.(type) { + case JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + case *JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + default: + rawKey = encryptionKey + } + + switch rcpt.Algorithm { + case DIRECT: + // Direct encryption mode must be treated differently + if reflect.TypeOf(rawKey) != reflect.TypeOf([]byte{}) { + return nil, ErrUnsupportedKeyType + } + encrypter.keyGenerator = staticKeyGenerator{ + key: rawKey.([]byte), + } + recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, rawKey.([]byte)) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + case ECDH_ES: + // ECDH-ES (w/o key wrapping) is similar to DIRECT mode + typeOf := reflect.TypeOf(rawKey) + if typeOf != reflect.TypeOf(&ecdsa.PublicKey{}) { + return nil, ErrUnsupportedKeyType + } + encrypter.keyGenerator = ecKeyGenerator{ + size: encrypter.cipher.keySize(), + algID: string(enc), + publicKey: rawKey.(*ecdsa.PublicKey), + } + recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, rawKey.(*ecdsa.PublicKey)) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + default: + // Can just add a standard recipient + encrypter.keyGenerator = randomKeyGenerator{ + size: encrypter.cipher.keySize(), + } + err := encrypter.addRecipient(rcpt) + return encrypter, err + } +} + +// NewMultiEncrypter creates a multi-encrypter based on the given parameters +func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { + cipher := getContentCipher(enc) + + if cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + if rcpts == nil || len(rcpts) == 0 { + return nil, fmt.Errorf("square/go-jose: recipients is nil or empty") + } + + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: cipher, + keyGenerator: randomKeyGenerator{ + size: cipher.keySize(), + }, + } + + if opts != nil { + encrypter.compressionAlg = opts.Compression + } + + for _, recipient := range rcpts { + err := encrypter.addRecipient(recipient) + if err != nil { + return nil, err + } + } + + return encrypter, nil +} + +func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { + var recipientInfo recipientKeyInfo + + switch recipient.Algorithm { + case DIRECT, ECDH_ES: + return fmt.Errorf("square/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) + } + + recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) + if recipient.KeyID != "" { + recipientInfo.keyID = recipient.KeyID + } + + switch recipient.Algorithm { + case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW: + if sr, ok := recipientInfo.keyEncrypter.(*symmetricKeyCipher); ok { + sr.p2c = recipient.PBES2Count + sr.p2s = recipient.PBES2Salt + } + } + + if err == nil { + ctx.recipients = append(ctx.recipients, recipientInfo) + } + return err +} + +func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { + switch encryptionKey := encryptionKey.(type) { + case *rsa.PublicKey: + return newRSARecipient(alg, encryptionKey) + case *ecdsa.PublicKey: + return newECDHRecipient(alg, encryptionKey) + case []byte: + return newSymmetricRecipient(alg, encryptionKey) + case string: + return newSymmetricRecipient(alg, []byte(encryptionKey)) + case *JSONWebKey: + recipient, err := makeJWERecipient(alg, encryptionKey.Key) + recipient.keyID = encryptionKey.KeyID + return recipient, err + default: + return recipientKeyInfo{}, ErrUnsupportedKeyType + } +} + +// newDecrypter creates an appropriate decrypter based on the key type +func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { + switch decryptionKey := decryptionKey.(type) { + case *rsa.PrivateKey: + return &rsaDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case *ecdsa.PrivateKey: + return &ecDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case []byte: + return &symmetricKeyCipher{ + key: decryptionKey, + }, nil + case string: + return &symmetricKeyCipher{ + key: []byte(decryptionKey), + }, nil + case JSONWebKey: + return newDecrypter(decryptionKey.Key) + case *JSONWebKey: + return newDecrypter(decryptionKey.Key) + default: + return nil, ErrUnsupportedKeyType + } +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { + return ctx.EncryptWithAuthData(plaintext, nil) +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { + obj := &JSONWebEncryption{} + obj.aad = aad + + obj.protected = &rawHeader{} + err := obj.protected.set(headerEncryption, ctx.contentAlg) + if err != nil { + return nil, err + } + + obj.recipients = make([]recipientInfo, len(ctx.recipients)) + + if len(ctx.recipients) == 0 { + return nil, fmt.Errorf("square/go-jose: no recipients to encrypt to") + } + + cek, headers, err := ctx.keyGenerator.genKey() + if err != nil { + return nil, err + } + + obj.protected.merge(&headers) + + for i, info := range ctx.recipients { + recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) + if err != nil { + return nil, err + } + + err = recipient.header.set(headerAlgorithm, info.keyAlg) + if err != nil { + return nil, err + } + + if info.keyID != "" { + err = recipient.header.set(headerKeyID, info.keyID) + if err != nil { + return nil, err + } + } + obj.recipients[i] = recipient + } + + if len(ctx.recipients) == 1 { + // Move per-recipient headers into main protected header if there's + // only a single recipient. + obj.protected.merge(obj.recipients[0].header) + obj.recipients[0].header = nil + } + + if ctx.compressionAlg != NONE { + plaintext, err = compress(ctx.compressionAlg, plaintext) + if err != nil { + return nil, err + } + + err = obj.protected.set(headerCompression, ctx.compressionAlg) + if err != nil { + return nil, err + } + } + + for k, v := range ctx.extraHeaders { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + (*obj.protected)[k] = makeRawMessage(b) + } + + authData := obj.computeAuthData() + parts, err := ctx.cipher.encrypt(cek, authData, plaintext) + if err != nil { + return nil, err + } + + obj.iv = parts.iv + obj.ciphertext = parts.ciphertext + obj.tag = parts.tag + + return obj, nil +} + +func (ctx *genericEncrypter) Options() EncrypterOptions { + return EncrypterOptions{ + Compression: ctx.compressionAlg, + ExtraHeaders: ctx.extraHeaders, + } +} + +// Decrypt and validate the object and return the plaintext. Note that this +// function does not support multi-recipient, if you desire multi-recipient +// decryption use DecryptMulti instead. +func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { + headers := obj.mergedHeaders(nil) + + if len(obj.recipients) > 1 { + return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one") + } + + critical, err := headers.getCritical() + if err != nil { + return nil, fmt.Errorf("square/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return nil, fmt.Errorf("square/go-jose: unsupported crit header") + } + + decrypter, err := newDecrypter(decryptionKey) + if err != nil { + return nil, err + } + + cipher := getContentCipher(headers.getEncryption()) + if cipher == nil { + return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + var plaintext []byte + recipient := obj.recipients[0] + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + } + + if plaintext == nil { + return nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + } + + return plaintext, err +} + +// DecryptMulti decrypts and validates the object and returns the plaintexts, +// with support for multiple recipients. It returns the index of the recipient +// for which the decryption was successful, the merged headers for that recipient, +// and the plaintext. +func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { + globalHeaders := obj.mergedHeaders(nil) + + critical, err := globalHeaders.getCritical() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported crit header") + } + + decrypter, err := newDecrypter(decryptionKey) + if err != nil { + return -1, Header{}, nil, err + } + + encryption := globalHeaders.getEncryption() + cipher := getContentCipher(encryption) + if cipher == nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(encryption)) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + index := -1 + var plaintext []byte + var headers rawHeader + + for i, recipient := range obj.recipients { + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + if err == nil { + index = i + headers = recipientHeaders + break + } + } + } + + if plaintext == nil || err != nil { + return -1, Header{}, nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + } + + sanitized, err := headers.sanitized() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: failed to sanitize header: %v", err) + } + + return index, sanitized, plaintext, err +} diff --git a/vendor/gopkg.in/square/go-jose.v2/doc.go b/vendor/gopkg.in/square/go-jose.v2/doc.go new file mode 100644 index 00000000000..dd1387f3f06 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/doc.go @@ -0,0 +1,27 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + +Package jose aims to provide an implementation of the Javascript Object Signing +and Encryption set of standards. It implements encryption and signing based on +the JSON Web Encryption and JSON Web Signature standards, with optional JSON +Web Token support available in a sub-package. The library supports both the +compact and full serialization formats, and has optional support for multiple +recipients. + +*/ +package jose diff --git a/vendor/gopkg.in/square/go-jose.v2/encoding.go b/vendor/gopkg.in/square/go-jose.v2/encoding.go new file mode 100644 index 00000000000..b9687c647d7 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/encoding.go @@ -0,0 +1,179 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "encoding/binary" + "io" + "math/big" + "regexp" + + "gopkg.in/square/go-jose.v2/json" +) + +var stripWhitespaceRegex = regexp.MustCompile("\\s") + +// Helper function to serialize known-good objects. +// Precondition: value is not a nil pointer. +func mustSerializeJSON(value interface{}) []byte { + out, err := json.Marshal(value) + if err != nil { + panic(err) + } + // We never want to serialize the top-level value "null," since it's not a + // valid JOSE message. But if a caller passes in a nil pointer to this method, + // MarshalJSON will happily serialize it as the top-level value "null". If + // that value is then embedded in another operation, for instance by being + // base64-encoded and fed as input to a signing algorithm + // (https://github.com/square/go-jose/issues/22), the result will be + // incorrect. Because this method is intended for known-good objects, and a nil + // pointer is not a known-good object, we are free to panic in this case. + // Note: It's not possible to directly check whether the data pointed at by an + // interface is a nil pointer, so we do this hacky workaround. + // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I + if string(out) == "null" { + panic("Tried to serialize a nil pointer.") + } + return out +} + +// Strip all newlines and whitespace +func stripWhitespace(data string) string { + return stripWhitespaceRegex.ReplaceAllString(data, "") +} + +// Perform compression based on algorithm +func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return deflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// Perform decompression based on algorithm +func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return inflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// Compress with DEFLATE +func deflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + + // Writing to byte buffer, err is always nil + writer, _ := flate.NewWriter(output, 1) + _, _ = io.Copy(writer, bytes.NewBuffer(input)) + + err := writer.Close() + return output.Bytes(), err +} + +// Decompress with DEFLATE +func inflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + reader := flate.NewReader(bytes.NewBuffer(input)) + + _, err := io.Copy(output, reader) + if err != nil { + return nil, err + } + + err = reader.Close() + return output.Bytes(), err +} + +// byteBuffer represents a slice of bytes that can be serialized to url-safe base64. +type byteBuffer struct { + data []byte +} + +func newBuffer(data []byte) *byteBuffer { + if data == nil { + return nil + } + return &byteBuffer{ + data: data, + } +} + +func newFixedSizeBuffer(data []byte, length int) *byteBuffer { + if len(data) > length { + panic("square/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") + } + pad := make([]byte, length-len(data)) + return newBuffer(append(pad, data...)) +} + +func newBufferFromInt(num uint64) *byteBuffer { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, num) + return newBuffer(bytes.TrimLeft(data, "\x00")) +} + +func (b *byteBuffer) MarshalJSON() ([]byte, error) { + return json.Marshal(b.base64()) +} + +func (b *byteBuffer) UnmarshalJSON(data []byte) error { + var encoded string + err := json.Unmarshal(data, &encoded) + if err != nil { + return err + } + + if encoded == "" { + return nil + } + + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return err + } + + *b = *newBuffer(decoded) + + return nil +} + +func (b *byteBuffer) base64() string { + return base64.RawURLEncoding.EncodeToString(b.data) +} + +func (b *byteBuffer) bytes() []byte { + // Handling nil here allows us to transparently handle nil slices when serializing. + if b == nil { + return nil + } + return b.data +} + +func (b byteBuffer) bigInt() *big.Int { + return new(big.Int).SetBytes(b.data) +} + +func (b byteBuffer) toInt() int { + return int(b.bigInt().Int64()) +} diff --git a/vendor/gopkg.in/square/go-jose.v2/json/LICENSE b/vendor/gopkg.in/square/go-jose.v2/json/LICENSE new file mode 100644 index 00000000000..74487567632 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gopkg.in/square/go-jose.v2/json/decode.go b/vendor/gopkg.in/square/go-jose.v2/json/decode.go new file mode 100644 index 00000000000..37457e5a834 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/decode.go @@ -0,0 +1,1183 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "errors" + "fmt" + "reflect" + "runtime" + "strconv" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. +// Unmarshal will only set exported fields of the struct. +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a string-keyed map, Unmarshal first +// establishes a map to use, If the map is nil, Unmarshal allocates a new map. +// Otherwise Unmarshal reuses the existing map, keeping existing entries. +// Unmarshal then stores key-value pairs from the JSON object into the map. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// ``not present,'' unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +// +func Unmarshal(data []byte, v interface{}) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by objects +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes +} + +func (e *UnmarshalTypeError) Error() string { + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// (No longer used; kept for compatibility.) +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + err = r.(error) + } + }() + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + d.value(rv) + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// isValidNumber reports whether s is a valid JSON number literal. +func isValidNumber(s string) bool { + // This function implements the JSON numbers grammar. + // See https://tools.ietf.org/html/rfc7159#section-6 + // and http://json.org/number.gif + + if s == "" { + return false + } + + // Optional - + if s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + + // Digits + switch { + default: + return false + + case s[0] == '0': + s = s[1:] + + case '1' <= s[0] && s[0] <= '9': + s = s[1:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // . followed by 1 or more digits. + if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { + s = s[2:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // e or E followed by an optional - or + and + // 1 or more digits. + if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { + s = s[1:] + if s[0] == '+' || s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // Make sure we are at the end. + return s == "" +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // read offset in data + scan scanner + nextscan scanner // for calls to nextValue + savedError error + useNumber bool +} + +// errPhase is used for errors that should not happen unless +// there is a bug in the JSON decoder or something is editing +// the data slice while the decoder executes. +var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + return d +} + +// error aborts the decoding by panicking with err. +func (d *decodeState) error(err error) { + panic(err) +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = err + } +} + +// next cuts off and returns the next full JSON value in d.data[d.off:]. +// The next value is known to be an object or array, not a literal. +func (d *decodeState) next() []byte { + c := d.data[d.off] + item, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // Our scanner has seen the opening brace/bracket + // and thinks we're still in the middle of the object. + // invent a closing brace/bracket to get it out. + if c == '{' { + d.scan.step(&d.scan, '}') + } else { + d.scan.step(&d.scan, ']') + } + + return item +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +// It updates d.off and returns the new scan code. +func (d *decodeState) scanWhile(op int) int { + var newOp int + for { + if d.off >= len(d.data) { + newOp = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } else { + c := d.data[d.off] + d.off++ + newOp = d.scan.step(&d.scan, c) + } + if newOp != op { + break + } + } + return newOp +} + +// value decodes a JSON value from d.data[d.off:] into the value. +// it updates d.off to point past the decoded value. +func (d *decodeState) value(v reflect.Value) { + if !v.IsValid() { + _, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // d.scan thinks we're still at the beginning of the item. + // Feed in an empty string - the shortest, simplest value - + // so that it knows we got to the end of the value. + if d.scan.redo { + // rewind. + d.scan.redo = false + d.scan.step = stateBeginValue + } + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + + n := len(d.scan.parseState) + if n > 0 && d.scan.parseState[n-1] == parseObjectKey { + // d.scan thinks we just read an object key; finish the object + d.scan.step(&d.scan, ':') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '}') + } + + return + } + + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(v) + + case scanBeginObject: + d.object(v) + + case scanBeginLiteral: + d.literal(v) + } +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() interface{} { + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(reflect.Value{}) + + case scanBeginObject: + d.object(reflect.Value{}) + + case scanBeginLiteral: + switch v := d.literalInterface().(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// if it encounters an Unmarshaler, indirect stops and returns that. +// if decodingNull is true, indirect stops at the last pointer so it can be set to nil. +func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + v = e + continue + } + } + + if v.Kind() != reflect.Ptr { + break + } + + if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + v = v.Elem() + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into the value v. +// the first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + } + + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + v.Set(reflect.ValueOf(d.arrayInterface())) + return + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + case reflect.Array: + case reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + d.value(v.Index(i)) + } else { + // Ran out of fixed array: skip. + d.value(reflect.Value{}) + } + i++ + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } +} + +var nullLiteral = []byte("null") + +// object consumes an object from d.data[d.off-1:], decoding into the value v. +// the first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + v = pv + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + v.Set(reflect.ValueOf(d.objectInterface())) + return + } + + // Check type of target: struct or map[string]T + switch v.Kind() { + case reflect.Map: + // map must have string kind + t := v.Type() + if t.Key().Kind() != reflect.String { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + + default: + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + + var mapElem reflect.Value + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := v.Type().Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.Set(reflect.Zero(elemType)) + } + subv = mapElem + } else { + var f *field + fields := cachedTypeFields(v.Type()) + for i := range fields { + ff := &fields[i] + if bytes.Equal(ff.nameBytes, []byte(key)) { + f = ff + break + } + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Ptr { + if subv.IsNil() { + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + } + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + d.literalStore(nullLiteral, subv, false) + case string: + d.literalStore([]byte(qv), subv, true) + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + d.value(subv) + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kv := reflect.ValueOf(key).Convert(v.Type().Key()) + v.SetMapIndex(kv, subv) + } + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } +} + +// literal consumes a literal from d.data[d.off-1:], decoding into the value v. +// The first byte of the literal has been read already +// (that's how the caller knows it's a literal). +func (d *decodeState) literal(v reflect.Value) { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + + d.literalStore(d.data[start:d.off], v, false) +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (interface{}, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} + } + return f, nil +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { + // Check for unmarshaler. + if len(item) == 0 { + //Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return + } + wantptr := item[0] == 'n' // null + u, ut, pv := d.indirect(v, wantptr) + if u != nil { + err := u.UnmarshalJSON(item) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + return + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + err := ut.UnmarshalText(s) + if err != nil { + d.error(err) + } + return + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + switch v.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + v.Set(reflect.Zero(v.Type())) + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := c == 't' + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + v.SetString(s) + if !isValidNumber(s) { + d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) + } + break + } + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + } + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetFloat(n) + } + } +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() interface{} { + switch d.scanWhile(scanSkipSpace) { + default: + d.error(errPhase) + panic("unreachable") + case scanBeginArray: + return d.arrayInterface() + case scanBeginObject: + return d.objectInterface() + case scanBeginLiteral: + return d.literalInterface() + } +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []interface{} { + var v = make([]interface{}, 0) + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]interface{} { + m := make(map[string]interface{}) + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read string key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } + return m +} + +// literalInterface is like literal but returns an interface value. +func (d *decodeState) literalInterface() interface{} { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + item := d.data[start:d.off] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + d.error(errPhase) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + d.error(errPhase) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + r, err := strconv.ParseUint(string(s[2:6]), 16, 64) + if err != nil { + return -1 + } + return rune(r) +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/vendor/gopkg.in/square/go-jose.v2/json/encode.go b/vendor/gopkg.in/square/go-jose.v2/json/encode.go new file mode 100644 index 00000000000..1dae8bb7cd8 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/encode.go @@ -0,0 +1,1197 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON objects as defined in +// RFC 4627. The mapping between JSON objects and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" +// to keep some browsers from misinterpreting JSON output as HTML. +// Ampersand "&" is also escaped to "\u0026" for the same reason. +// +// Array and slice values encode as JSON arrays, except that +// []byte encodes as a base64-encoded string, and a nil slice +// encodes as the null JSON object. +// +// Struct values encode as JSON objects. Each exported struct field +// becomes a member of the object unless +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option. +// The empty values are false, 0, any +// nil pointer or interface value, and any array, slice, map, or string of +// length zero. The object's default key string is the struct field name +// but can be specified in the struct field's tag value. The "json" key in +// the struct field's tag value is the key name, followed by an optional comma +// and options. Examples: +// +// // Field is ignored by this package. +// Field int `json:"-"` +// +// // Field appears in JSON as key "myName". +// Field int `json:"myName"` +// +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is empty, +// // as defined above. +// Field int `json:"myName,omitempty"` +// +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if empty. +// // Note the leading comma. +// Field int `json:",omitempty"` +// +// The "string" option signals that a field is stored as JSON inside a +// JSON-encoded string. It applies only to fields of string, floating point, +// integer, or boolean types. This extra level of encoding is sometimes used +// when communicating with JavaScript programs: +// +// Int64String int64 `json:",string"` +// +// The key name will be used if it's a non-empty string consisting of +// only Unicode letters, digits, dollar signs, percent signs, hyphens, +// underscores and slashes. +// +// Anonymous struct fields are usually marshaled as if their inner exported fields +// were fields in the outer struct, subject to the usual Go visibility rules amended +// as described in the next paragraph. +// An anonymous struct field with a name given in its JSON tag is treated as +// having that name, rather than being anonymous. +// An anonymous struct field of interface type is treated the same as having +// that type as its name, rather than being anonymous. +// +// The Go visibility rules for struct fields are amended for JSON when +// deciding which field to marshal or unmarshal. If there are +// multiple fields at the same level, and that level is the least +// nested (and would therefore be the nesting level selected by the +// usual Go rules), the following extra rules apply: +// +// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, +// even if there are multiple untagged fields that would otherwise conflict. +// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. +// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. +// +// Handling of anonymous struct fields is new in Go 1.1. +// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of +// an anonymous struct field in both current and earlier versions, give the field +// a JSON tag of "-". +// +// Map values encode as JSON objects. +// The map's key type must be string; the map keys are used as JSON object +// keys, subject to the UTF-8 coercion described for string values above. +// +// Pointer values encode as the value pointed to. +// A nil pointer encodes as the null JSON object. +// +// Interface values encode as the value contained in the interface. +// A nil interface value encodes as the null JSON object. +// +// Channel, complex, and function values cannot be encoded in JSON. +// Attempting to encode such a value causes Marshal to return +// an UnsupportedTypeError. +// +// JSON cannot represent cyclic data structures and Marshal does not +// handle them. Passing cyclic structures to Marshal will result in +// an infinite recursion. +// +func Marshal(v interface{}) ([]byte, error) { + e := &encodeState{} + err := e.marshal(v) + if err != nil { + return nil, err + } + return e.Bytes(), nil +} + +// MarshalIndent is like Marshal but applies Indent to format the output. +func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { + b, err := Marshal(v) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = Indent(&buf, b, prefix, indent) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 +// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 +// so that the JSON will be safe to embed inside HTML