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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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/118] 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 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 024/118] 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 025/118] 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 026/118] 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 027/118] 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 028/118] 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 029/118] 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 030/118] 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 031/118] 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 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 032/118] 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 033/118] 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 034/118] 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 3c7e54461f0278c20129a061f0249ee74a015e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 18 Sep 2018 15:32:06 +0200 Subject: [PATCH 035/118] updated --- .../dashboard/dashgrid/PanelEditor.tsx | 11 ++++--- .../dashboard/dashgrid/PanelHeader.tsx | 33 ++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index a9b7802fb4b..03cc5350fee 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -2,11 +2,11 @@ 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 { store } from 'app/store/configureStore'; import { QueriesTab } from './QueriesTab'; import { PanelPlugin, PluginExports } from 'app/types/plugins'; import { VizTypePicker } from './VizTypePicker'; +import { updateLocation } from 'app/core/actions'; interface PanelEditorProps { panel: PanelModel; @@ -22,7 +22,6 @@ interface PanelEditorTab { icon: string; } -@observer export class PanelEditor extends React.Component { tabs: PanelEditorTab[]; @@ -65,7 +64,11 @@ export class PanelEditor extends React.Component { } onChangeTab = (tab: PanelEditorTab) => { - store.view.updateQuery({ tab: tab.id }, false); + store.dispatch( + updateLocation({ + query: { tab: tab.id }, + }) + ); }; render() { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx index 02d905a1e68..12d5cd37253 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -2,7 +2,8 @@ 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 { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; interface PanelHeaderProps { panel: PanelModel; @@ -11,24 +12,26 @@ interface PanelHeaderProps { export class PanelHeader extends React.Component { onEditPanel = () => { - store.view.updateQuery( - { - panelId: this.props.panel.id, - edit: true, - fullscreen: true, - }, - false + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panel.id, + edit: true, + fullscreen: true, + }, + }) ); }; onViewPanel = () => { - store.view.updateQuery( - { - panelId: this.props.panel.id, - fullscreen: true, - edit: false, - }, - false + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panel.id, + edit: false, + fullscreen: true, + }, + }) ); }; From 5b522254502e501912c0f2226a12c47389fb46b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 20 Sep 2018 09:01:06 +0200 Subject: [PATCH 036/118] wip: panel options idea2 --- .../app/features/panel/metrics_panel_ctrl.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 3 +- public/app/features/panel/panel_editor_tab.ts | 3 +- .../features/panel/partials/metrics_tab.html | 4 -- public/app/features/panel/viz_tab.ts | 49 ++++++++++++++----- public/app/plugins/panel/graph/module.ts | 4 +- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index ce65d11b3a1..c7468216881 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -56,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); + // this.addCommonTab('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 82a4a116f89..fbef69bc42a 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -100,11 +100,12 @@ export class PanelCtrl { 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); + // this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + const urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase(); if (urlTab) { this.editorTabs.forEach((tab, i) => { diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index d16bab3dc55..f7e1c48a323 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -13,9 +13,8 @@ function panelEditorTab(dynamicDirectiveSrv) { }, directive: scope => { const pluginId = scope.ctrl.pluginId; - const tabName = scope.editorTab.title.toLowerCase(); + const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-'); - console.log('panelEditorTab', pluginId, tabName); if (directiveCache[pluginId]) { if (directiveCache[pluginId][tabName]) { return directiveCache[pluginId][tabName]; diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index 0ee1f81b0c3..815a99d6b74 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -1,11 +1,7 @@
    - - -
    - -
    -
    -
    -
    {{tab.title}}
    - -
    -
    -
    -`; +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    `; /** @ngInject */ export function vizTabDirective() { @@ -46,5 +70,4 @@ export function vizTabDirective() { } react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]); - coreModule.directive('vizTab', vizTabDirective); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index f0751ddd816..d9d67f11b6c 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -134,12 +134,12 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 5); + this.addCommonTab('Alert', alertTab, 5); } this.subTabIndex = 0; From b7ba73819c1d541f0b8ead9ed18bf35530105045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 4 Oct 2018 09:14:47 +0200 Subject: [PATCH 037/118] mini fix --- public/app/features/dashboard/dashgrid/PanelEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 03cc5350fee..2a6228217ed 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -72,7 +72,8 @@ export class PanelEditor extends React.Component { }; render() { - const activeTab: string = store.view.query.get('tab') || 'queries'; + const { location } = store.getState(); + const activeTab = location.query.tab || 'queries'; return (
    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 038/118] 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 d2bdf8cff12e33016704e78331e6cb9dd104caac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 9 Oct 2018 17:07:13 +0200 Subject: [PATCH 039/118] wip: fixed issues now things are starting to work as before for angular panels --- public/app/core/constants.ts | 3 +++ public/app/features/panel/metrics_panel_ctrl.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 4 ++-- public/app/features/panel/panel_directive.ts | 6 ------ public/app/features/plugins/plugin_component.ts | 12 ++++++++---- public/app/partials/dashboard.html | 2 +- public/sass/pages/_dashboard.scss | 4 ++++ 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 2642c5e400a..00981156614 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; export const LS_PANEL_COPY_KEY = 'panel-copy'; + +export const DASHBOARD_TOOLBAR_HEIGHT = 55; +export const DASHBOARD_TOP_PADDING = 20; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 67848e460fb..1cdf7c6953d 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -57,7 +57,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Queries', metricsTabDirective, 1, 'fa fa-database'); + this.addEditorTab('Metrics', metricsTabDirective, 1, 'fa fa-database'); 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 51dd38e4358..cbdfd632e0b 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -220,8 +220,8 @@ export class PanelCtrl { calculatePanelHeight() { if (this.panel.fullscreen) { - const docHeight = $(window).height(); - const editHeight = Math.floor(docHeight * 0.4); + const docHeight = $('.react-grid-layout').height(); + const editHeight = Math.floor(docHeight * 0.35); const fullscreenHeight = Math.floor(docHeight * 0.8); this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight; } else { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index def7c69a69d..e4d70ff3cf9 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -90,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.dashboard.setPanelFocus(0); } - function panelHeightUpdated() { - // panelContent.css({ height: ctrl.height + 'px' }); - } - function resizeScrollableContent() { if (panelScrollbar) { panelScrollbar.update(); @@ -138,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.events.on('panel-size-changed', () => { ctrl.calculatePanelHeight(); - panelHeightUpdated(); $timeout(() => { console.log('panel directive panel size changed, render'); resizeScrollableContent(); @@ -148,7 +143,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { // set initial height ctrl.calculatePanelHeight(); - panelHeightUpdated(); ctrl.events.on('render', () => { console.log('panel_directive: render', ctrl.panel.id); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 47f2fc535e8..4b421ca446a 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader'; import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module'; /** @ngInject */ -function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) { +function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) { function getTemplate(component) { if (component.template) { return $q.when(component.template); @@ -207,10 +207,14 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // let a binding digest cycle complete before adding to dom setTimeout(() => { - elem.append(child); scope.$applyAsync(() => { - console.log('post appendAndCompile, broadcast refresh', scope.panel); - scope.$broadcast('component-did-mount'); + elem.append(child); + setTimeout(() => { + scope.$applyAsync(() => { + console.log('post appendAndCompile, broadcast refresh', scope.panel); + scope.$broadcast('component-did-mount'); + }); + }); }); }); } diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 9e7d4fa1c6c..32acdc435f2 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -7,7 +7,7 @@ class="dashboard-settings"> -
      +
      diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 13801df8cd1..d9ab29cc91c 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -3,6 +3,10 @@ width: 100%; height: 100%; box-sizing: border-box; + + &--has-submenu { + height: calc(100% - 50px); + } } .template-variable { From 565edc1ed36e14746ce72fa4bd53fb6ed0c7f734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 9 Oct 2018 17:47:43 +0200 Subject: [PATCH 040/118] added setting top hide plugins in alpha state --- conf/defaults.ini | 3 + pkg/api/api.go | 106 +++++++++--------- pkg/api/frontendsettings.go | 10 +- pkg/api/http_server.go | 2 +- pkg/api/index.go | 12 +- pkg/api/login.go | 4 +- pkg/api/user.go | 6 +- pkg/setting/setting.go | 5 + .../{text2 => graph2}/img/icn-text-panel.svg | 0 public/app/plugins/panel/graph2/module.tsx | 12 +- public/app/plugins/panel/graph2/plugin.json | 4 +- .../{graph2 => text2}/img/icn-graph-panel.svg | 0 public/app/plugins/panel/text2/module.tsx | 12 +- public/app/plugins/panel/text2/plugin.json | 8 +- 14 files changed, 99 insertions(+), 85 deletions(-) rename public/app/plugins/panel/{text2 => graph2}/img/icn-text-panel.svg (100%) rename public/app/plugins/panel/{graph2 => text2}/img/icn-graph-panel.svg (100%) diff --git a/conf/defaults.ini b/conf/defaults.ini index eb8debc0094..750f06f2f6a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -554,3 +554,6 @@ container_name = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer server_url = callback_url = + +[panels] +enable_alpha = false diff --git a/pkg/api/api.go b/pkg/api/api.go index 39b332aeb9f..9e350960a84 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -22,66 +22,66 @@ func (hs *HTTPServer) registerRoutes() { r := hs.RouteRegister // not logged in views - r.Get("/", reqSignedIn, Index) + r.Get("/", reqSignedIn, hs.Index) r.Get("/logout", Logout) r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost)) r.Get("/login/:name", quota("session"), OAuthLogin) - r.Get("/login", LoginView) - r.Get("/invite/:code", Index) + r.Get("/login", hs.LoginView) + r.Get("/invite/:code", hs.Index) // authed views - r.Get("/profile/", reqSignedIn, Index) - r.Get("/profile/password", reqSignedIn, Index) - r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome) - r.Get("/org/", reqSignedIn, Index) - r.Get("/org/new", reqSignedIn, Index) - r.Get("/datasources/", reqSignedIn, Index) - r.Get("/datasources/new", reqSignedIn, Index) - r.Get("/datasources/edit/*", reqSignedIn, Index) - r.Get("/org/users", reqSignedIn, Index) - r.Get("/org/users/new", reqSignedIn, Index) - r.Get("/org/users/invite", reqSignedIn, Index) - r.Get("/org/teams", reqSignedIn, Index) - r.Get("/org/teams/*", reqSignedIn, Index) - r.Get("/org/apikeys/", reqSignedIn, Index) - r.Get("/dashboard/import/", reqSignedIn, Index) - r.Get("/configuration", reqGrafanaAdmin, Index) - r.Get("/admin", reqGrafanaAdmin, Index) - r.Get("/admin/settings", reqGrafanaAdmin, Index) - r.Get("/admin/users", reqGrafanaAdmin, Index) - r.Get("/admin/users/create", reqGrafanaAdmin, Index) - r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) - r.Get("/admin/orgs", reqGrafanaAdmin, Index) - r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) - r.Get("/admin/stats", reqGrafanaAdmin, Index) + r.Get("/profile/", reqSignedIn, hs.Index) + r.Get("/profile/password", reqSignedIn, hs.Index) + r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome) + r.Get("/org/", reqSignedIn, hs.Index) + r.Get("/org/new", reqSignedIn, hs.Index) + r.Get("/datasources/", reqSignedIn, hs.Index) + r.Get("/datasources/new", reqSignedIn, hs.Index) + r.Get("/datasources/edit/*", reqSignedIn, hs.Index) + r.Get("/org/users", reqSignedIn, hs.Index) + r.Get("/org/users/new", reqSignedIn, hs.Index) + r.Get("/org/users/invite", reqSignedIn, hs.Index) + r.Get("/org/teams", reqSignedIn, hs.Index) + r.Get("/org/teams/*", reqSignedIn, hs.Index) + r.Get("/org/apikeys/", reqSignedIn, hs.Index) + r.Get("/dashboard/import/", reqSignedIn, hs.Index) + r.Get("/configuration", reqGrafanaAdmin, hs.Index) + r.Get("/admin", reqGrafanaAdmin, hs.Index) + r.Get("/admin/settings", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index) + r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index) + r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index) + r.Get("/admin/stats", reqGrafanaAdmin, hs.Index) - r.Get("/styleguide", reqSignedIn, Index) + r.Get("/styleguide", reqSignedIn, hs.Index) - r.Get("/plugins", reqSignedIn, Index) - r.Get("/plugins/:id/edit", reqSignedIn, Index) - r.Get("/plugins/:id/page/:page", reqSignedIn, Index) + r.Get("/plugins", reqSignedIn, hs.Index) + r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) + r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index) - r.Get("/d/:uid/:slug", reqSignedIn, Index) - r.Get("/d/:uid", reqSignedIn, Index) - r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index) - r.Get("/dashboard/script/*", reqSignedIn, Index) - r.Get("/dashboard-solo/snapshot/*", Index) - r.Get("/d-solo/:uid/:slug", reqSignedIn, Index) - r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index) - r.Get("/dashboard-solo/script/*", reqSignedIn, Index) - r.Get("/import/dashboard", reqSignedIn, Index) - r.Get("/dashboards/", reqSignedIn, Index) - r.Get("/dashboards/*", reqSignedIn, Index) + r.Get("/d/:uid/:slug", reqSignedIn, hs.Index) + r.Get("/d/:uid", reqSignedIn, hs.Index) + r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, hs.Index) + r.Get("/dashboard/script/*", reqSignedIn, hs.Index) + r.Get("/dashboard-solo/snapshot/*", hs.Index) + r.Get("/d-solo/:uid/:slug", reqSignedIn, hs.Index) + r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, hs.Index) + r.Get("/dashboard-solo/script/*", reqSignedIn, hs.Index) + r.Get("/import/dashboard", reqSignedIn, hs.Index) + r.Get("/dashboards/", reqSignedIn, hs.Index) + r.Get("/dashboards/*", reqSignedIn, hs.Index) - r.Get("/explore", reqEditorRole, Index) + r.Get("/explore", reqEditorRole, hs.Index) - r.Get("/playlists/", reqSignedIn, Index) - r.Get("/playlists/*", reqSignedIn, Index) - r.Get("/alerting/", reqSignedIn, Index) - r.Get("/alerting/*", reqSignedIn, Index) + r.Get("/playlists/", reqSignedIn, hs.Index) + r.Get("/playlists/*", reqSignedIn, hs.Index) + r.Get("/alerting/", reqSignedIn, hs.Index) + r.Get("/alerting/*", reqSignedIn, hs.Index) // sign up - r.Get("/signup", Index) + r.Get("/signup", hs.Index) r.Get("/api/user/signup/options", Wrap(GetSignUpOptions)) r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp)) r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2)) @@ -91,15 +91,15 @@ func (hs *HTTPServer) registerRoutes() { r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite)) // reset password - r.Get("/user/password/send-reset-email", Index) - r.Get("/user/password/reset", Index) + r.Get("/user/password/send-reset-email", hs.Index) + r.Get("/user/password/reset", hs.Index) r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail)) r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword)) // dashboard snapshots - r.Get("/dashboard/snapshot/*", Index) - r.Get("/dashboard/snapshots/", reqSignedIn, Index) + r.Get("/dashboard/snapshot/*", hs.Index) + r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index) // api for dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) @@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() { pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting)) }, reqOrgAdmin) - apiRoute.Get("/frontend/settings/", GetFrontendSettings) + apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings) apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest) apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a58be38781e..4ef1f66200c 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { +func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { orgDataSources := make([]*m.DataSource, 0) if c.OrgId != 0 { @@ -120,6 +120,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { panels := map[string]interface{}{} for _, panel := range enabledPlugins.Panels { + if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels { + continue + } + panels[panel.Id] = map[string]interface{}{ "module": panel.Module, "baseUrl": panel.BaseUrl, @@ -183,8 +187,8 @@ func getPanelSort(id string) int { return sort } -func GetFrontendSettings(c *m.ReqContext) { - settings, err := getFrontendSettingsMap(c) +func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) { + settings, err := hs.getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) return diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 432d6a18369..65d1d359f95 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -184,7 +184,7 @@ func (hs *HTTPServer) applyRoutes() { // then custom app proxy routes hs.initAppPluginRoutes(hs.macaron) // lastly not found route - hs.macaron.NotFound(NotFoundHandler) + hs.macaron.NotFound(hs.NotFoundHandler) } func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { diff --git a/pkg/api/index.go b/pkg/api/index.go index 1b73acd8829..61fb7ec3a5b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -17,8 +17,8 @@ const ( darkName = "dark" ) -func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { - settings, err := getFrontendSettingsMap(c) +func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { + settings, err := hs.getFrontendSettingsMap(c) if err != nil { return nil, err } @@ -353,8 +353,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { return &data, nil } -func Index(c *m.ReqContext) { - data, err := setIndexViewData(c) +func (hs *HTTPServer) Index(c *m.ReqContext) { + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return @@ -362,13 +362,13 @@ func Index(c *m.ReqContext) { c.HTML(200, "index", data) } -func NotFoundHandler(c *m.ReqContext) { +func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return } - data, err := setIndexViewData(c) + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return diff --git a/pkg/api/login.go b/pkg/api/login.go index 632d04e37f1..1083f89adfd 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -17,8 +17,8 @@ const ( ViewIndex = "index" ) -func LoginView(c *m.ReqContext) { - viewData, err := setIndexViewData(c) +func (hs *HTTPServer) LoginView(c *m.ReqContext) { + viewData, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return diff --git a/pkg/api/user.go b/pkg/api/user.go index 4b916202e65..7116ad83f3f 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -177,17 +177,17 @@ func UserSetUsingOrg(c *m.ReqContext) Response { } // GET /profile/switch-org/:id -func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { +func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { orgID := c.ParamsInt64(":id") if !validateUsingOrg(c.UserId, orgID) { - NotFoundHandler(c) + hs.NotFoundHandler(c) } cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { - NotFoundHandler(c) + hs.NotFoundHandler(c) } c.Redirect(setting.AppSubUrl + "/") diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 27df73a9eed..12d452468d1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -209,6 +209,8 @@ type Cfg struct { TempDataLifetime time.Duration MetricsEndpointEnabled bool + + EnableAlphaPanels bool } type CommandLineArgs struct { @@ -688,6 +690,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(false) + panels := iniFile.Section("panels") + cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false) + cfg.readSessionConfig() cfg.readSmtpSettings() cfg.readQuotaSettings() diff --git a/public/app/plugins/panel/text2/img/icn-text-panel.svg b/public/app/plugins/panel/graph2/img/icn-text-panel.svg similarity index 100% rename from public/app/plugins/panel/text2/img/icn-text-panel.svg rename to public/app/plugins/panel/graph2/img/icn-text-panel.svg diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 7ae24bd7d40..a0df192f9d8 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; -export class ReactTestPanel extends PureComponent { +export class Graph2 extends PureComponent { constructor(props) { super(props); } @@ -14,8 +14,14 @@ export class ReactTestPanel extends PureComponent { value = data[0].value; } - return

      Graph Panel! {value}

      ; + return

      Text Panel {value}

      ; } } -export { ReactTestPanel as PanelComponent }; +export class TextOptions extends PureComponent { + render() { + return

      Text2 Options component

      ; + } +} + +export { Graph2 as PanelComponent, TextOptions as PanelOptions }; diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json index bc60d6ad2d7..b519a57fae4 100644 --- a/public/app/plugins/panel/graph2/plugin.json +++ b/public/app/plugins/panel/graph2/plugin.json @@ -9,8 +9,8 @@ "url": "https://grafana.com" }, "logos": { - "small": "img/icn-graph-panel.svg", - "large": "img/icn-graph-panel.svg" + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" } } } diff --git a/public/app/plugins/panel/graph2/img/icn-graph-panel.svg b/public/app/plugins/panel/text2/img/icn-graph-panel.svg similarity index 100% rename from public/app/plugins/panel/graph2/img/icn-graph-panel.svg rename to public/app/plugins/panel/text2/img/icn-graph-panel.svg diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 703a9897c6f..df00b85176a 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; -export class ReactTestPanel extends PureComponent { +export class Text2 extends PureComponent { constructor(props) { super(props); } @@ -14,14 +14,8 @@ export class ReactTestPanel extends PureComponent { value = data[0].value; } - return

      Text Panel {value}

      ; + return

      Graph Panel! {value}

      ; } } -export class TextOptions extends PureComponent { - render() { - return

      Text2 Options component

      ; - } -} - -export { ReactTestPanel as PanelComponent, TextOptions as PanelOptions }; +export { Text2 as PanelComponent }; diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json index 95d821bfd50..b1133f65e36 100644 --- a/public/app/plugins/panel/text2/plugin.json +++ b/public/app/plugins/panel/text2/plugin.json @@ -1,16 +1,18 @@ { "type": "panel", - "name": "Text2", + "name": "Text v2", "id": "text2", + "state": "alpha", + "info": { "author": { "name": "Grafana Project", "url": "https://grafana.com" }, "logos": { - "small": "img/icn-text-panel.svg", - "large": "img/icn-text-panel.svg" + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" } } } From ee1d4ce0e2dc465c2213f8242cfb26dfb5ebe9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 10 Oct 2018 09:26:17 +0200 Subject: [PATCH 041/118] fix tab switching --- public/app/core/reducers/location.ts | 11 +++++++++-- .../app/features/dashboard/dashgrid/PanelEditor.tsx | 1 + public/app/types/location.ts | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 2089cfe9f59..7c7dffd04b9 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -1,6 +1,7 @@ import { Action } from 'app/core/actions/location'; import { LocationState } from 'app/types'; import { renderUrl } from 'app/core/utils/url'; +import _ from 'lodash'; export const initialState: LocationState = { url: '', @@ -12,11 +13,17 @@ export const initialState: LocationState = { export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { - const { path, query, routeParams } = action.payload; + const { path, routeParams } = action.payload; + let query = action.payload.query || state.query; + + if (action.payload.partial) { + query = _.defaults(query, state.query); + } + return { url: renderUrl(path || state.path, query), path: path || state.path, - query: query || state.query, + query: query, routeParams: routeParams || state.routeParams, }; } diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 2a6228217ed..26ac8b7d2c1 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -67,6 +67,7 @@ export class PanelEditor extends React.Component { store.dispatch( updateLocation({ query: { tab: tab.id }, + partial: true, }) ); }; diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 4a7f51523a7..7dcf57f7e02 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -2,6 +2,7 @@ export interface LocationUpdate { path?: string; query?: UrlQueryMap; routeParams?: UrlQueryMap; + partial?: boolean; } export interface LocationState { From 0662b5f962eb43a92ea7bba2e7eaeb4da6eabebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 11:13:04 +0200 Subject: [PATCH 042/118] fixing unit tests --- .../app/features/dashboard/dashgrid/QueriesTab.tsx | 14 ++++++++++---- public/app/features/dashboard/panel_model.ts | 2 -- .../features/dashboard/specs/viewstate_srv.test.ts | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 179d074d547..21159df00aa 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -21,15 +21,21 @@ export class QueriesTab extends React.Component { return; } + const { panel, dashboard } = this.props; + + // make sure the panel has datasource & queries properties + panel.datasource = panel.datasource || null; + panel.targets = panel.targets || [{}]; + const loader = getAngularLoader(); const template = ''; const scopeProps = { ctrl: { - panel: this.props.panel, - dashboard: this.props.dashboard, + panel: panel, + dashboard: dashboard, panelCtrl: { - panel: this.props.panel, - dashboard: this.props.dashboard, + panel: panel, + dashboard: dashboard, }, }, }; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index a3053ae48f8..415238d4e7b 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -52,8 +52,6 @@ export class PanelModel { // 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/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index c2d9a28ea51..f9963afbf85 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -11,6 +11,7 @@ describe('when updating view state', () => { }; const $scope = { + appEvent: jest.fn(), onAppEvent: jest.fn(() => {}), dashboard: new DashboardModel({ panels: [{ id: 1 }], From 2e1d45a8756d99c8c5eaaee2f28bb9a22c0b4a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 12:41:09 +0200 Subject: [PATCH 043/118] changed DataPanel from HOC to use render props --- public/app/core/components/grafana_app.ts | 2 +- public/app/core/services/angular_loader.ts | 42 ----- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/DataPanel.tsx | 151 ++++++++++-------- .../dashboard/dashgrid/PanelChrome.tsx | 24 +-- .../dashboard/dashgrid/QueriesTab.tsx | 2 +- public/app/plugins/panel/graph2/module.tsx | 10 +- 7 files changed, 106 insertions(+), 127 deletions(-) delete mode 100644 public/app/core/services/angular_loader.ts diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index b513bed3ef0..a244a5393d1 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -9,7 +9,7 @@ import Drop from 'tether-drop'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { AngularLoader, setAngularLoader } from 'app/core/services/angular_loader'; +import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { diff --git a/public/app/core/services/angular_loader.ts b/public/app/core/services/angular_loader.ts deleted file mode 100644 index e3a7dec4351..00000000000 --- a/public/app/core/services/angular_loader.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - const 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/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 039473a7a30..7dd8a06996d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader'; +import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; import { importPluginModule } from 'app/features/plugins/plugin_loader'; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index bce4c69bb69..d19d6b10e2e 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,81 +1,92 @@ -import React, { Component, ComponentClass } from 'react'; +// Library +import React, { Component } from 'react'; -export interface OuterProps { - type: string; +interface RenderProps { + loading: LoadingState; + data: any; +} + +export interface Props { + datasource: string | null; queries: any[]; - isVisible: boolean; + children: (r: RenderProps) => JSX.Element; } -export interface PanelProps extends OuterProps { - data: any[]; +export interface State { + isFirstLoad: boolean; + loading: LoadingState; + data: any; } -export interface DataPanel extends ComponentClass {} - -interface State { - isLoading: boolean; - data: any[]; +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', } -export const DataPanelWrapper = (ComposedComponent: ComponentClass) => { - class Wrapper extends Component { - static defaultProps = { - isVisible: true, +export interface PanelProps extends RenderProps {} + +export class DataPanel extends Component { + constructor(props: Props) { + super(props); + + this.state = { + loading: LoadingState.NotStarted, + data: [], + isFirstLoad: true, }; - - constructor(props: OuterProps) { - super(props); - - this.state = { - isLoading: false, - data: [], - }; - } - - componentDidMount() { - console.log('data panel mount'); - this.issueQueries(); - } - - issueQueries = async () => { - const { isVisible } = this.props; - - if (!isVisible) { - return; - } - - this.setState({ isLoading: true }); - - await new Promise(resolve => { - setTimeout(() => { - this.setState({ isLoading: false, data: [{ value: 10 }] }); - }, 500); - }); - }; - - render() { - const { data, isLoading } = this.state; - console.log('data panel render'); - - if (!data.length) { - return ( -
      -

      No Data

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

      Loading

      -
      - ); - } - - return ; - } } - return Wrapper; -}; + componentDidMount() { + console.log('DataPanel mount'); + this.issueQueries(); + } + + issueQueries = async () => { + this.setState({ loading: LoadingState.Loading }); + + await new Promise(resolve => { + setTimeout(() => { + this.setState({ loading: LoadingState.Done, data: [{ value: 10 }], isFirstLoad: false }); + }, 500); + }); + }; + + render() { + const { data, loading, isFirstLoad } = this.state; + console.log('data panel render'); + + if (isFirstLoad && loading === LoadingState.Loading) { + return ( +
      +

      Loading

      +
      + ); + } + + return ( + <> + {this.loadingSpinner} + {this.props.children({ + data, + loading, + })} + + ); + } + + private get loadingSpinner(): JSX.Element { + const { loading } = this.state; + + if (loading === LoadingState.Loading) { + return ( +
      + +
      + ); + } + + return null; + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 73208c34130..a3e4b4eedc8 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -2,7 +2,7 @@ import React, { ComponentClass } from 'react'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelHeader } from './PanelHeader'; -import { DataPanel, PanelProps, DataPanelWrapper } from './DataPanel'; +import { DataPanel, PanelProps } from './DataPanel'; export interface Props { panel: PanelModel; @@ -12,9 +12,6 @@ export interface Props { interface State {} -// cache DataPanel wrapper components -const dataPanels: { [s: string]: DataPanel } = {}; - export class PanelChrome extends React.Component { panelComponent: DataPanel; @@ -23,20 +20,25 @@ export class PanelChrome extends React.Component { } render() { - const { type } = this.props.panel; + const { datasource, targets } = this.props.panel; + const PanelComponent = this.props.component; - let PanelComponent = dataPanels[type]; - - if (!PanelComponent) { - PanelComponent = dataPanels[type] = DataPanelWrapper(this.props.component); - } + // if (!PanelComponent) { + // PanelComponent = dataPanels[type] = DataPanelWrapper(this.props.component); + // } console.log('PanelChrome render', PanelComponent); return (
      -
      {}
      +
      + + {({ loading, data }) => { + return ; + }} + +
      ); } diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 21159df00aa..671075b8c47 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,7 +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'; +import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; interface Props { panel: PanelModel; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index a0df192f9d8..79bf1890bae 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,7 +1,15 @@ import React, { PureComponent } from 'react'; import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; -export class Graph2 extends PureComponent { +interface Options { + showBars: boolean; +} + +interface Props extends PanelProps { + options: Options; +} + +export class Graph2 extends PureComponent { constructor(props) { super(props); } From cd4619dad17b695d7724504d6ad9ecba10434a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 15:39:34 +0200 Subject: [PATCH 044/118] working on react data / query exectution --- public/app/core/components/grafana_app.ts | 6 +- .../features/dashboard/dashgrid/DataPanel.tsx | 70 ++++++++++++++----- .../dashboard/dashgrid/PanelChrome.tsx | 70 ++++++++++++++----- public/app/features/dashboard/time_srv.ts | 30 +++++--- public/app/features/panel/panel_directive.ts | 1 - public/app/features/plugins/datasource_srv.ts | 10 +++ .../cloudwatch/query_parameter_ctrl.ts | 5 +- .../datasource/elasticsearch/bucket_agg.ts | 7 +- .../datasource/elasticsearch/metric_agg.ts | 7 +- .../datasource/graphite/add_graphite_func.ts | 4 +- .../datasource/graphite/func_editor.ts | 4 +- .../stackdriver/query_aggregation_ctrl.ts | 6 +- .../stackdriver/query_filter_ctrl.ts | 6 +- public/app/plugins/panel/graph/graph.ts | 1 - public/app/plugins/panel/graph/legend.ts | 6 +- .../panel/graph/series_overrides_ctrl.ts | 4 +- public/app/plugins/panel/graph2/module.tsx | 4 +- .../app/plugins/panel/heatmap/color_legend.ts | 8 +-- public/app/plugins/panel/text2/module.tsx | 4 +- public/app/types/index.ts | 5 ++ public/app/types/panel.ts | 6 ++ public/app/types/queries.ts | 19 +++++ 22 files changed, 204 insertions(+), 79 deletions(-) create mode 100644 public/app/types/panel.ts create mode 100644 public/app/types/queries.ts diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index a244a5393d1..d6291c94a6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -8,7 +8,8 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv'; +import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; import { configureStore } from 'app/store/configureStore'; @@ -23,12 +24,15 @@ export class GrafanaCtrl { contextSrv, bridgeSrv, backendSrv: BackendSrv, + timeSrv: TimeSrv, datasourceSrv: DatasourceSrv, angularLoader: AngularLoader ) { // make angular loader service available to react components setAngularLoader(angularLoader); setBackendSrv(backendSrv); + setDatasourceSrv(datasourceSrv); + setTimeSrv(timeSrv); configureStore(); $scope.init = () => { diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index d19d6b10e2e..6b8aa8c4a91 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,5 +1,11 @@ // Library -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; + +// Services +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + +// Types +import { TimeRange, LoadingState } from 'app/types'; interface RenderProps { loading: LoadingState; @@ -9,6 +15,11 @@ interface RenderProps { export interface Props { datasource: string | null; queries: any[]; + panelId?: number; + dashboardId?: number; + isVisible?: boolean; + timeRange?: TimeRange; + refreshCounter: number; children: (r: RenderProps) => JSX.Element; } @@ -18,16 +29,13 @@ export interface State { data: any; } -export enum LoadingState { - NotStarted = 'NotStarted', - Loading = 'Loading', - Done = 'Done', - Error = 'Error', -} +export class DataPanel extends PureComponent { + static defaultProps = { + isVisible: true, + panelId: 1, + dashboardId: 1, + }; -export interface PanelProps extends RenderProps {} - -export class DataPanel extends Component { constructor(props: Props) { super(props); @@ -44,20 +52,50 @@ export class DataPanel extends Component { } issueQueries = async () => { + const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; + + if (!isVisible) { + return; + } + + if (!queries.length) { + this.setState({ data: [], loading: LoadingState.Done }); + return; + } + this.setState({ loading: LoadingState.Loading }); - await new Promise(resolve => { - setTimeout(() => { - this.setState({ loading: LoadingState.Done, data: [{ value: 10 }], isFirstLoad: false }); - }, 500); - }); + try { + const dataSourceSrv = getDatasourceSrv(); + const ds = await dataSourceSrv.get(datasource); + + const queryOptions = { + timezone: 'browser', + panelId: panelId, + dashboardId: dashboardId, + range: timeRange, + rangeRaw: timeRange.raw, + interval: '1s', + intervalMs: 1000, + targets: queries, + maxDataPoints: 500, + scopedVars: {}, + cacheTimeout: null, + }; + + const resp = await ds.query(queryOptions); + console.log(resp); + } catch (err) { + console.log('Loading error', err); + this.setState({ loading: LoadingState.Error }); + } }; render() { const { data, loading, isFirstLoad } = this.state; console.log('data panel render'); - if (isFirstLoad && loading === LoadingState.Loading) { + if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { return (

      Loading

      diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index a3e4b4eedc8..29797664cd9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -1,8 +1,17 @@ -import React, { ComponentClass } from 'react'; +// Libraries +import React, { ComponentClass, PureComponent } from 'react'; + +// Services +import { getTimeSrv } from '../time_srv'; + +// Components +import { PanelHeader } from './PanelHeader'; +import { DataPanel } from './DataPanel'; + +// Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { PanelHeader } from './PanelHeader'; -import { DataPanel, PanelProps } from './DataPanel'; +import { TimeRange, PanelProps } from 'app/types'; export interface Props { panel: PanelModel; @@ -10,30 +19,59 @@ export interface Props { component: ComponentClass; } -interface State {} - -export class PanelChrome extends React.Component { - panelComponent: DataPanel; +export interface State { + refreshCounter: number; + timeRange?: TimeRange; +} +export class PanelChrome extends PureComponent { constructor(props) { super(props); + + this.state = { + refreshCounter: 0, + }; + } + + componentDidMount() { + this.props.dashboard.panelInitialized(this.props.panel); + this.props.panel.events.on('refresh', this.onRefresh); + } + + componentWillUnmount() { + this.props.panel.events.off('refresh', this.onRefresh); + } + + onRefresh = () => { + const timeSrv = getTimeSrv(); + const timeRange = timeSrv.timeRange(); + + this.setState({ + refreshCounter: this.state.refreshCounter + 1, + timeRange: timeRange, + }); + }; + + get isVisible() { + return this.props.dashboard.otherPanelInFullscreen(this.props.panel); } render() { - const { datasource, targets } = this.props.panel; + const { panel, dashboard } = this.props; + const { datasource, targets } = panel; + const { refreshCounter } = this.state; const PanelComponent = this.props.component; - // if (!PanelComponent) { - // PanelComponent = dataPanels[type] = DataPanelWrapper(this.props.component); - // } - - console.log('PanelChrome render', PanelComponent); - return (
      - +
      - + {({ loading, data }) => { return ; }} diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 89aa94ed336..06cadfa6530 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -1,9 +1,15 @@ +// Libraries import moment from 'moment'; import _ from 'lodash'; -import coreModule from 'app/core/core_module'; + +// Utils import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; +// Types +import { TimeRange } from 'app/types'; + export class TimeSrv { time: any; refreshTimer: any; @@ -200,7 +206,7 @@ export class TimeSrv { return range; } - timeRange() { + timeRange(): TimeRange { // make copies if they are moment (do not want to return out internal moment, because they are mutable!) const raw = { from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, @@ -222,17 +228,21 @@ export class TimeSrv { const timespan = range.to.valueOf() - range.from.valueOf(); const center = range.to.valueOf() - timespan / 2; - let to = center + timespan * factor / 2; - let from = center - timespan * factor / 2; - - if (to > Date.now() && range.to <= Date.now()) { - const offset = to - Date.now(); - from = from - offset; - to = Date.now(); - } + const to = center + timespan * factor / 2; + const from = center - timespan * factor / 2; this.setTime({ from: moment.utc(from), to: moment.utc(to) }); } } +let singleton; + +export function setTimeSrv(srv: TimeSrv) { + singleton = srv; +} + +export function getTimeSrv(): TimeSrv { + return singleton; +} + coreModule.service('timeSrv', TimeSrv); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index e4d70ff3cf9..d4554a2eed9 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -145,7 +145,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.calculatePanelHeight(); 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; diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 7ef82519668..22861eaeef8 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -162,5 +162,15 @@ export class DatasourceSrv { } } +let singleton: DatasourceSrv; + +export function setDatasourceSrv(srv: DatasourceSrv) { + singleton = srv; +} + +export function getDatasourceSrv(): DatasourceSrv { + return singleton; +} + coreModule.service('datasourceSrv', DatasourceSrv); export default DatasourceSrv; diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts index 4f4b2961761..ba5a39688b3 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; export class CloudWatchQueryParameter { @@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl { } } -angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter); -angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); +coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter); +coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index 8963f2c3f4b..cacf86201fe 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticBucketAgg', elasticBucketAgg); -module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); +coreModule.directive('elasticBucketAgg', elasticBucketAgg); +coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 623eed68914..1dd0d892360 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticMetricAgg', elasticMetricAgg); -module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); +coreModule.directive('elasticMetricAgg', elasticMetricAgg); +coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts index a5c1dc49959..ea3dfe8ff5e 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.ts +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -1,8 +1,8 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; import Drop from 'tether-drop'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteAddFunc($compile) { @@ -130,7 +130,7 @@ export function graphiteAddFunc($compile) { }; } -angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc); +coreModule.directive('graphiteAddFunc', graphiteAddFunc); function createFunctionDropDownMenu(funcDefs) { const categories = {}; diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 68cc6f1452e..9e19083a9c3 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -1,7 +1,7 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { @@ -315,4 +315,4 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { }; } -angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor); +coreModule.directive('graphiteFuncEditor', graphiteFuncEditor); diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 98a1258cb15..6cd6c805463 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as options from './constants'; import kbn from 'app/core/utils/kbn'; @@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl { } } -angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); -angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); +coreModule.directive('stackdriverAggregation', StackdriverAggregation); +coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 786b2831e89..7af76720d23 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; import appEvents from 'app/core/app_events'; @@ -281,5 +281,5 @@ export class StackdriverFilterCtrl { } } -angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter); -angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl); +coreModule.directive('stackdriverFilter', StackdriverFilter); +coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ec4b16fbc79..7a8e24539f7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -80,7 +80,6 @@ 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'); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index cf317389941..7b01c46c4d3 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -1,11 +1,9 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import baron from 'baron'; +import coreModule from 'app/core/core_module'; -const module = angular.module('grafana.directives'); - -module.directive('graphLegend', (popoverSrv, $timeout) => { +coreModule.directive('graphLegend', (popoverSrv, $timeout) => { return { link: (scope, elem) => { let firstRender = true; diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index deb7bd8ba61..540d19fb47a 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import angular from 'angular'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { @@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.updateCurrentOverrides(); } -angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); +coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 79bf1890bae..c5245137211 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; +import { PanelProps } from 'app/types'; interface Options { showBars: boolean; @@ -22,6 +22,8 @@ export class Graph2 extends PureComponent { value = data[0].value; } + console.log('graph2 render'); + return

      Text Panel {value}

      ; } } diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 628186569dd..0e011e59439 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -1,12 +1,10 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import * as d3 from 'd3'; import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; - -const module = angular.module('grafana.directives'); +import coreModule from 'app/core/core_module'; const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0; /** * Color legend for heatmap editor. */ -module.directive('colorLegend', () => { +coreModule.directive('colorLegend', () => { return { restrict: 'E', template: '
      ', @@ -52,7 +50,7 @@ module.directive('colorLegend', () => { /** * Heatmap legend with scale values. */ -module.directive('heatmapLegend', () => { +coreModule.directive('heatmapLegend', () => { return { restrict: 'E', template: `
      `, diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index df00b85176a..9fcd89b612c 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel'; +import { PanelProps } from 'app/types'; export class Text2 extends PureComponent { constructor(props) { @@ -14,7 +14,7 @@ export class Text2 extends PureComponent { value = data[0].value; } - return

      Graph Panel! {value}

      ; + return

      Text Panel! {value}

      ; } } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 26f15d582ac..9a1045f8c7c 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,6 +9,8 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; +import { TimeRange, LoadingState } from './queries'; +import { PanelProps } from './panel'; export { Team, @@ -45,6 +47,9 @@ export { OrgUser, User, UsersState, + TimeRange, + LoadingState, + PanelProps, }; export interface StoreState { diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts new file mode 100644 index 00000000000..4cb93e5728d --- /dev/null +++ b/public/app/types/panel.ts @@ -0,0 +1,6 @@ +import { LoadingState } from './queries'; + +export interface PanelProps { + data: any; + loading: LoadingState; +} diff --git a/public/app/types/queries.ts b/public/app/types/queries.ts new file mode 100644 index 00000000000..5c350efbb08 --- /dev/null +++ b/public/app/types/queries.ts @@ -0,0 +1,19 @@ +import { Moment } from 'moment'; + +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', +} + +export interface RawTimeRange { + from: Moment | string; + to: Moment | string; +} + +export interface TimeRange { + from: Moment; + to: Moment; + raw: RawTimeRange; +} From 543c67a2974e1cf58c2cf210216c5f1a334a1992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 16:31:20 +0200 Subject: [PATCH 045/118] react panels: got data --- public/app/app.ts | 45 ++++++++----------- public/app/core/core.ts | 2 - public/app/core/core_module.ts | 19 +++++++- .../features/dashboard/dashgrid/DataPanel.tsx | 16 ++++++- .../dashboard/dashgrid/PanelChrome.tsx | 7 +-- .../dashboard/dashgrid/QueriesTab.tsx | 4 -- public/app/features/panel/metrics_tab.ts | 3 ++ .../grafana_app.ts => routes/GrafanaCtrl.ts} | 0 8 files changed, 57 insertions(+), 39 deletions(-) rename public/app/{core/components/grafana_app.ts => routes/GrafanaCtrl.ts} (100%) diff --git a/public/app/app.ts b/public/app/app.ts index 298bf5609cd..9647fbe5416 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => { return array; }; -import { coreModule, registerAngularDirectives } from './core/core'; -import { setupAngularRoutes } from './routes/routes'; +import { coreModule, angularModules } from 'app/core/core_module'; +import { registerAngularDirectives } from 'app/core/core'; +import { setupAngularRoutes } from 'app/routes/routes'; + +import 'app/routes/GrafanaCtrl'; +import 'app/features/all'; // import symlinked extensions const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/); @@ -109,39 +113,26 @@ export class GrafanaApp { 'react', ]; - const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; - - _.each(moduleTypes, type => { - const moduleName = 'grafana.' + type; - this.useModule(angular.module(moduleName, [])); - }); - // makes it possible to add dynamic stuff - this.useModule(coreModule); + _.each(angularModules, m => { + this.useModule(m); + }); // register react angular wrappers coreModule.config(setupAngularRoutes); registerAngularDirectives(); - const preBootRequires = [import('app/features/all')]; + // disable tool tip animation + $.fn.tooltip.defaults.animation = false; - Promise.all(preBootRequires) - .then(() => { - // disable tool tip animation - $.fn.tooltip.defaults.animation = false; - - // bootstrap the app - angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { - _.extend(module, this.registerFunctions); - }); - - this.preBootModules = null; - }); - }) - .catch(err => { - console.log('Application boot failed:', err); + // bootstrap the app + angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { + _.each(this.preBootModules, module => { + _.extend(module, this.registerFunctions); }); + + this.preBootModules = null; + }); } } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 173d6b80b15..18a625d3307 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; -import { grafanaAppDirective } from './components/grafana_app'; import { searchDirective } from './components/search/search'; import { infoPopover } from './components/info_popover'; import { navbarDirective } from './components/navbar/navbar'; @@ -60,7 +59,6 @@ export { registerAngularDirectives, arrayJoin, coreModule, - grafanaAppDirective, navbarDirective, searchDirective, liveSrv, diff --git a/public/app/core/core_module.ts b/public/app/core/core_module.ts index f6c30e6cf15..94b0aeaff7f 100644 --- a/public/app/core/core_module.ts +++ b/public/app/core/core_module.ts @@ -1,2 +1,19 @@ import angular from 'angular'; -export default angular.module('grafana.core', ['ngRoute']); + +console.log('core module code'); +const coreModule = angular.module('grafana.core', ['ngRoute']); + +// legacy modules +const angularModules = [ + coreModule, + angular.module('grafana.controllers', []), + angular.module('grafana.directives', []), + angular.module('grafana.factories', []), + angular.module('grafana.services', []), + angular.module('grafana.filters', []), + angular.module('grafana.routes', []), +]; + +export { angularModules, coreModule }; + +export default coreModule; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 6b8aa8c4a91..a786e9de6d1 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,5 +1,5 @@ // Library -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -29,7 +29,7 @@ export interface State { data: any; } -export class DataPanel extends PureComponent { +export class DataPanel extends Component { static defaultProps = { isVisible: true, panelId: 1, @@ -48,9 +48,20 @@ export class DataPanel extends PureComponent { componentDidMount() { console.log('DataPanel mount'); + } + + async componentDidUpdate(prevProps: Props) { + if (!this.hasPropsChanged(prevProps)) { + return; + } + this.issueQueries(); } + hasPropsChanged(prevProps: Props) { + return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible; + } + issueQueries = async () => { const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; @@ -83,6 +94,7 @@ export class DataPanel extends PureComponent { cacheTimeout: null, }; + console.log('issueing react query', queryOptions); const resp = await ds.query(queryOptions); console.log(resp); } catch (err) { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 29797664cd9..9a4318e602f 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -34,8 +34,8 @@ export class PanelChrome extends PureComponent { } componentDidMount() { - this.props.dashboard.panelInitialized(this.props.panel); this.props.panel.events.on('refresh', this.onRefresh); + this.props.dashboard.panelInitialized(this.props.panel); } componentWillUnmount() { @@ -53,13 +53,13 @@ export class PanelChrome extends PureComponent { }; get isVisible() { - return this.props.dashboard.otherPanelInFullscreen(this.props.panel); + return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } render() { const { panel, dashboard } = this.props; const { datasource, targets } = panel; - const { refreshCounter } = this.state; + const { refreshCounter, timeRange } = this.state; const PanelComponent = this.props.component; return ( @@ -69,6 +69,7 @@ export class PanelChrome extends PureComponent { diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 671075b8c47..efc6a6c0048 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -23,10 +23,6 @@ export class QueriesTab extends React.Component { const { panel, dashboard } = this.props; - // make sure the panel has datasource & queries properties - panel.datasource = panel.datasource || null; - panel.targets = panel.targets || [{}]; - const loader = getAngularLoader(); const template = ''; const scopeProps = { diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index c5f945778de..18b1102875e 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -25,6 +25,9 @@ export class MetricsTabCtrl { $scope.ctrl = this; this.panel = this.panelCtrl.panel; + this.panel.datasource = this.panel.datasource || null; + this.panel.targets = this.panel.targets || [{}]; + this.dashboard = this.panelCtrl.dashboard; this.datasources = datasourceSrv.getMetricSources(); this.panelDsValue = this.panelCtrl.panel.datasource; diff --git a/public/app/core/components/grafana_app.ts b/public/app/routes/GrafanaCtrl.ts similarity index 100% rename from public/app/core/components/grafana_app.ts rename to public/app/routes/GrafanaCtrl.ts From 8e85295b2b981442dc0a18bfec8128389eba258d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 18:19:49 +0200 Subject: [PATCH 046/118] react panels query processing --- public/app/core/core_module.ts | 1 - .../features/dashboard/dashgrid/DataPanel.tsx | 26 ++++++---- .../dashboard/dashgrid/PanelChrome.tsx | 4 +- public/app/features/plugins/datasource_srv.ts | 8 ++- public/app/plugins/panel/graph/module.ts | 1 + public/app/plugins/panel/graph2/module.tsx | 30 +++++++---- public/app/plugins/panel/text2/module.tsx | 9 +--- public/app/types/index.ts | 6 ++- public/app/types/panel.ts | 4 +- public/app/types/queries.ts | 19 ------- public/app/types/series.ts | 50 +++++++++++++++++++ 11 files changed, 106 insertions(+), 52 deletions(-) delete mode 100644 public/app/types/queries.ts create mode 100644 public/app/types/series.ts diff --git a/public/app/core/core_module.ts b/public/app/core/core_module.ts index 94b0aeaff7f..c8401975c18 100644 --- a/public/app/core/core_module.ts +++ b/public/app/core/core_module.ts @@ -1,6 +1,5 @@ import angular from 'angular'; -console.log('core module code'); const coreModule = angular.module('grafana.core', ['ngRoute']); // legacy modules diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index a786e9de6d1..8014e06f43f 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -5,11 +5,11 @@ import React, { Component } from 'react'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Types -import { TimeRange, LoadingState } from 'app/types'; +import { TimeRange, LoadingState, DataQueryResponse, TimeSeries } from 'app/types'; interface RenderProps { loading: LoadingState; - data: any; + timeSeries: TimeSeries[]; } export interface Props { @@ -26,7 +26,7 @@ export interface Props { export interface State { isFirstLoad: boolean; loading: LoadingState; - data: any; + response: DataQueryResponse; } export class DataPanel extends Component { @@ -41,7 +41,9 @@ export class DataPanel extends Component { this.state = { loading: LoadingState.NotStarted, - data: [], + response: { + data: [], + }, isFirstLoad: true, }; } @@ -70,7 +72,7 @@ export class DataPanel extends Component { } if (!queries.length) { - this.setState({ data: [], loading: LoadingState.Done }); + this.setState({ loading: LoadingState.Done }); return; } @@ -94,9 +96,14 @@ export class DataPanel extends Component { cacheTimeout: null, }; - console.log('issueing react query', queryOptions); + console.log('Issuing DataPanel query', queryOptions); const resp = await ds.query(queryOptions); - console.log(resp); + console.log('Issuing DataPanel query Resp', resp); + + this.setState({ + loading: LoadingState.Done, + response: resp, + }); } catch (err) { console.log('Loading error', err); this.setState({ loading: LoadingState.Error }); @@ -104,8 +111,9 @@ export class DataPanel extends Component { }; render() { - const { data, loading, isFirstLoad } = this.state; + const { response, loading, isFirstLoad } = this.state; console.log('data panel render'); + const timeSeries = response.data; if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { return ( @@ -119,7 +127,7 @@ export class DataPanel extends Component { <> {this.loadingSpinner} {this.props.children({ - data, + timeSeries, loading, })} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 9a4318e602f..d3e63d93e43 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -73,8 +73,8 @@ export class PanelChrome extends PureComponent { isVisible={this.isVisible} refreshCounter={refreshCounter} > - {({ loading, data }) => { - return ; + {({ loading, timeSeries }) => { + return ; }}
      diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 22861eaeef8..71a417a882f 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -1,8 +1,14 @@ +// Libraries import _ from 'lodash'; import coreModule from 'app/core/core_module'; + +// Utils import config from 'app/core/config'; import { importPluginModule } from './plugin_loader'; +// Types +import { DataSourceApi } from 'app/types/series'; + export class DatasourceSrv { datasources: any; @@ -15,7 +21,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name?) { + get(name?): Promise { if (!name) { return this.get(config.defaultDatasource); } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 584b58ae3ce..1ebda67d41a 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -179,6 +179,7 @@ class GraphCtrl extends MetricsPanelCtrl { } onDataReceived(dataList) { + console.log(dataList); this.dataList = dataList; this.seriesList = this.processor.getSeriesList({ dataList: dataList, diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index c5245137211..7132efc9ac0 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,4 +1,8 @@ +// Libraries +import _ from 'lodash'; import React, { PureComponent } from 'react'; + +// Types import { PanelProps } from 'app/types'; interface Options { @@ -15,16 +19,24 @@ export class Graph2 extends PureComponent { } render() { - const { data } = this.props; - let value = 0; + const { timeSeries } = this.props; + let index = 0; - if (data.length) { - value = data[0].value; - } - - console.log('graph2 render'); - - return

      Text Panel {value}

      ; + return ( + + + {timeSeries.map(series => { + return ( + + + + + + ); + })} + +
      {series.target}{series.datapoints[0][0]}{series.datapoints[0][1]}
      + ); } } diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 9fcd89b612c..b10dc8b545e 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -7,14 +7,7 @@ export class Text2 extends PureComponent { } render() { - const { data } = this.props; - let value = 0; - - if (data.length) { - value = data[0].value; - } - - return

      Text Panel! {value}

      ; + return

      Text Panel!

      ; } } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 9a1045f8c7c..f13a55b5361 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,7 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; -import { TimeRange, LoadingState } from './queries'; +import { TimeRange, LoadingState, TimeSeries, DataQuery, DataQueryResponse, DataQueryOptions } from './series'; import { PanelProps } from './panel'; export { @@ -50,6 +50,10 @@ export { TimeRange, LoadingState, PanelProps, + TimeSeries, + DataQuery, + DataQueryResponse, + DataQueryOptions, }; export interface StoreState { diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 4cb93e5728d..8d12cb6ef21 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,6 +1,6 @@ -import { LoadingState } from './queries'; +import { LoadingState, TimeSeries } from './series'; export interface PanelProps { - data: any; + timeSeries: TimeSeries[]; loading: LoadingState; } diff --git a/public/app/types/queries.ts b/public/app/types/queries.ts deleted file mode 100644 index 5c350efbb08..00000000000 --- a/public/app/types/queries.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Moment } from 'moment'; - -export enum LoadingState { - NotStarted = 'NotStarted', - Loading = 'Loading', - Done = 'Done', - Error = 'Error', -} - -export interface RawTimeRange { - from: Moment | string; - to: Moment | string; -} - -export interface TimeRange { - from: Moment; - to: Moment; - raw: RawTimeRange; -} diff --git a/public/app/types/series.ts b/public/app/types/series.ts new file mode 100644 index 00000000000..006410a5757 --- /dev/null +++ b/public/app/types/series.ts @@ -0,0 +1,50 @@ +import { Moment } from 'moment'; + +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', +} + +export interface RawTimeRange { + from: Moment | string; + to: Moment | string; +} + +export interface TimeRange { + from: Moment; + to: Moment; + raw: RawTimeRange; +} + +export type TimeSeriesValue = string | number | null; + +export type TimeSeriesPoints = TimeSeriesValue[][]; + +export interface TimeSeries { + target: string; + datapoints: TimeSeriesPoints; +} + +export interface DataQueryResponse { + data: TimeSeries[]; +} + +export interface DataQuery { + refId: string; +} + +export interface DataQueryOptions { + timezone: string; + range: TimeRange; + rangeRaw: RawTimeRange; + targets: DataQuery[]; + panelId: number; + dashboardId: number; + cacheTimeout?: string; +} + +export interface DataSourceApi { + query(options: DataQueryOptions): Promise; +} From 2fb721d3c684420b85736776afed47534b8c91f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 21:14:11 +0200 Subject: [PATCH 047/118] various fixes to to queries tab (in react mode) --- .../dashboard/dashgrid/QueriesTab.tsx | 16 +++++----- public/app/features/dashboard/panel_model.ts | 12 +++++++- .../features/dashboard/specs/exporter.test.ts | 2 +- .../app/features/panel/metrics_panel_ctrl.ts | 22 -------------- public/app/features/panel/metrics_tab.ts | 30 +++++++++++++++++-- public/app/plugins/panel/graph/module.ts | 1 - 6 files changed, 49 insertions(+), 34 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index efc6a6c0048..f13f212826a 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,14 +1,19 @@ -import React from 'react'; +// Libraries +import React, { PureComponent } from 'react'; + +// Services & utils +import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; + +// Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; interface Props { panel: PanelModel; dashboard: DashboardModel; } -export class QueriesTab extends React.Component { +export class QueriesTab extends PureComponent { element: any; component: AngularComponent; @@ -29,10 +34,7 @@ export class QueriesTab extends React.Component { ctrl: { panel: panel, dashboard: dashboard, - panelCtrl: { - panel: panel, - dashboard: dashboard, - }, + refresh: () => panel.refresh(), }, }; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 415238d4e7b..ebf8a6bb224 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -16,6 +16,12 @@ const notPersistedProperties: { [str: string]: boolean } = { hasRefreshed: true, }; +const defaults: any = { + gridPos: { x: 0, y: 0, h: 3, w: 6 }, + datasource: null, + targets: [{}], +}; + export class PanelModel { id: number; gridPos: GridPos; @@ -51,7 +57,7 @@ export class PanelModel { } // defaults - this.gridPos = this.gridPos || { x: 0, y: 0, h: 3, w: 6 }; + _.defaultsDeep(this, _.cloneDeep(defaults)); } getSaveModel() { @@ -61,6 +67,10 @@ export class PanelModel { continue; } + if (_.isEqual(this[property], defaults[property])) { + continue; + } + model[property] = _.cloneDeep(this[property]); } diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/specs/exporter.test.ts index c7a232f925b..f21e151f3dd 100644 --- a/public/app/features/dashboard/specs/exporter.test.ts +++ b/public/app/features/dashboard/specs/exporter.test.ts @@ -240,5 +240,5 @@ stubs['-- Grafana --'] = { }; function getStub(arg) { - return Promise.resolve(stubs[arg]); + return Promise.resolve(stubs[arg || 'gfdb']); } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 1cdf7c6953d..aa25308e121 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -12,7 +12,6 @@ import { metricsTabDirective } from './metrics_tab'; class MetricsPanelCtrl extends PanelCtrl { scope: any; datasource: any; - datasourceName: any; $q: any; $timeout: any; contextSrv: any; @@ -287,27 +286,6 @@ class MetricsPanelCtrl extends PanelCtrl { }); } - setDatasource(datasource) { - // switching to mixed - if (datasource.meta.mixed) { - _.each(this.panel.targets, target => { - target.datasource = this.panel.datasource; - if (!target.datasource) { - target.datasource = config.defaultDatasource; - } - }); - } else if (this.datasource && this.datasource.meta.mixed) { - _.each(this.panel.targets, target => { - delete target.datasource; - }); - } - - this.panel.datasource = datasource.value; - this.datasourceName = datasource.name; - this.datasource = null; - this.refresh(); - } - getAdditionalMenuItems() { const items = []; if ( diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 18b1102875e..f520b5eefc0 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,6 +1,13 @@ -import { DashboardModel } from '../dashboard/dashboard_model'; +// Libraries +import _ from 'lodash'; import Remarkable from 'remarkable'; + +// Services & utils import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; export class MetricsTabCtrl { dsName: string; @@ -70,10 +77,29 @@ export class MetricsTabCtrl { } this.datasourceInstance = option.datasource; - this.panelCtrl.setDatasource(option.datasource); + this.setDatasource(option.datasource); this.updateDatasourceOptions(); } + setDatasource(datasource) { + // switching to mixed + if (datasource.meta.mixed) { + _.each(this.panel.targets, target => { + target.datasource = this.panel.datasource; + if (!target.datasource) { + target.datasource = config.defaultDatasource; + } + }); + } else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) { + _.each(this.panel.targets, target => { + delete target.datasource; + }); + } + + this.panel.datasource = datasource.value; + this.panel.refresh(); + } + addMixedQuery(option) { if (!option) { return; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 1ebda67d41a..584b58ae3ce 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -179,7 +179,6 @@ class GraphCtrl extends MetricsPanelCtrl { } onDataReceived(dataList) { - console.log(dataList); this.dataList = dataList; this.seriesList = this.processor.getSeriesList({ dataList: dataList, From 9adad76f5211b00257e255c5786ef8155ed03beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 14 Oct 2018 22:24:18 +0200 Subject: [PATCH 048/118] wip: began first steps for a react graph component --- public/app/plugins/panel/graph2/module.tsx | 19 +-- public/app/types/index.ts | 11 +- public/app/types/panel.ts | 3 +- public/app/types/series.ts | 7 + public/app/viz/Graph.tsx | 126 ++++++++++++++++++ .../app/viz/state/getTimeSeriesViewModel.ts | 9 ++ 6 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 public/app/viz/Graph.tsx create mode 100644 public/app/viz/state/getTimeSeriesViewModel.ts diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 7132efc9ac0..a984e6a2a6f 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -2,6 +2,9 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; +// Components +import { Graph } from 'app/viz/Graph'; + // Types import { PanelProps } from 'app/types'; @@ -22,21 +25,7 @@ export class Graph2 extends PureComponent { const { timeSeries } = this.props; let index = 0; - return ( - - - {timeSeries.map(series => { - return ( - - - - - - ); - })} - -
      {series.target}{series.datapoints[0][0]}{series.datapoints[0][1]}
      - ); + return ; } } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index f13a55b5361..8d827a6c286 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,15 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginMeta, Plugin, PluginsState } from './plugins'; -import { TimeRange, LoadingState, TimeSeries, DataQuery, DataQueryResponse, DataQueryOptions } from './series'; +import { + TimeRange, + LoadingState, + TimeSeries, + DataQuery, + DataQueryResponse, + DataQueryOptions, + TimeSeriesViewModel, +} from './series'; import { PanelProps } from './panel'; export { @@ -51,6 +59,7 @@ export { LoadingState, PanelProps, TimeSeries, + TimeSeriesViewModel, DataQuery, DataQueryResponse, DataQueryOptions, diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 8d12cb6ef21..5ece77fc5aa 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,6 +1,7 @@ -import { LoadingState, TimeSeries } from './series'; +import { LoadingState, TimeSeries, TimeRange } from './series'; export interface PanelProps { timeSeries: TimeSeries[]; + timeRange: TimeRange; loading: LoadingState; } diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 006410a5757..dc1d1d7d393 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -25,6 +25,13 @@ export type TimeSeriesPoints = TimeSeriesValue[][]; export interface TimeSeries { target: string; datapoints: TimeSeriesPoints; + unit?: string; +} + +export interface TimeSeriesViewModel { + label: string; + color: string; + data: number[][]; } export interface DataQueryResponse { diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx new file mode 100644 index 00000000000..ed638e7bd16 --- /dev/null +++ b/public/app/viz/Graph.tsx @@ -0,0 +1,126 @@ +// Libraries +import $ from 'jquery'; +import React, { PureComponent } from 'react'; +import { withSize } from 'react-sizeme'; +import 'vendor/flot/jquery.flot'; +import 'vendor/flot/jquery.flot.time'; + +// Types +import TimeSeries from 'app/core/time_series2'; +import { TimeRange } from 'app/types'; + +// Copied from graph.ts +function time_format(ticks, min, max) { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +const FLOT_OPTIONS = { + legend: { + show: false, + }, + series: { + lines: { + linewidth: 1, + zero: false, + }, + shadowSize: 0, + }, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + // hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, +}; + +interface GraphProps { + timeSeries: TimeSeries[]; + timeRange: TimeRange; + size?: { width: number; height: number }; +} + +export class Graph extends PureComponent { + element: any; + + componentDidUpdate(prevProps: GraphProps) { + if ( + prevProps.timeSeries !== this.props.timeSeries || + prevProps.timeRange !== this.props.timeRange || + prevProps.size !== this.props.size + ) { + this.draw(); + } + } + + componentDidMount() { + this.draw(); + } + + draw() { + const { size, timeSeries, timeRange } = this.props; + + const data = timeSeries.map((ts: TimeSeries) => ({ + color: ts.color, + label: ts.label, + data: ts.getFlotPairs('null'), + })); + + const ticks = (size.width || 0) / 100; + const min = timeRange.from.valueOf(); + const max = timeRange.to.valueOf(); + + const dynamicOptions = { + xaxis: { + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: time_format(ticks, min, max), + }, + }; + + const options = { + ...FLOT_OPTIONS, + ...dynamicOptions, + }; + + $.plot(this.element, data, options); + } + + render() { + return ( +
      +
      (this.element = e)} /> +
      + ); + } +} + +export default withSize()(Graph); diff --git a/public/app/viz/state/getTimeSeriesViewModel.ts b/public/app/viz/state/getTimeSeriesViewModel.ts new file mode 100644 index 00000000000..9ad2216d135 --- /dev/null +++ b/public/app/viz/state/getTimeSeriesViewModel.ts @@ -0,0 +1,9 @@ +import colors from 'app/core/utils/colors'; +import { TimeSeries, TimeSeriesViewModel } from 'app/types'; + +interface Options { + ts: TimeSeries; + seriesIndex: number; +} + +export function getTimeSeriesViewModel(ts: TimeSeries): TimeSeriesViewModel {} From a28c25a2e026ed5c0544a93b9d3f9baa077525e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 15 Oct 2018 08:22:20 +0200 Subject: [PATCH 049/118] progress on react time series infra --- .../dashboard/dashgrid/PanelChrome.tsx | 2 +- public/app/plugins/panel/graph2/module.tsx | 12 +++++++---- public/app/types/index.ts | 6 ++++-- public/app/types/series.ts | 10 +++++++-- public/app/viz/Graph.tsx | 15 ++++++------- .../app/viz/state/getTimeSeriesViewModel.ts | 9 -------- public/app/viz/state/timeSeries.ts | 21 +++++++++++++++++++ 7 files changed, 48 insertions(+), 27 deletions(-) delete mode 100644 public/app/viz/state/getTimeSeriesViewModel.ts create mode 100644 public/app/viz/state/timeSeries.ts diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index d3e63d93e43..82b366d8126 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -74,7 +74,7 @@ export class PanelChrome extends PureComponent { refreshCounter={refreshCounter} > {({ loading, timeSeries }) => { - return ; + return ; }}
      diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index a984e6a2a6f..e8d594e7968 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -3,7 +3,10 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Components -import { Graph } from 'app/viz/Graph'; +import Graph from 'app/viz/Graph'; + +// Utils +import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; // Types import { PanelProps } from 'app/types'; @@ -22,10 +25,11 @@ export class Graph2 extends PureComponent { } render() { - const { timeSeries } = this.props; - let index = 0; + const { timeSeries, timeRange } = this.props; + const viewModels = getTimeSeriesVMs({ timeSeries }); + console.log(viewModels); - return ; + return ; } } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8d827a6c286..d274c99253c 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -13,10 +13,11 @@ import { TimeRange, LoadingState, TimeSeries, + TimeSeriesVM, + TimeSeriesVMs, DataQuery, DataQueryResponse, DataQueryOptions, - TimeSeriesViewModel, } from './series'; import { PanelProps } from './panel'; @@ -59,7 +60,8 @@ export { LoadingState, PanelProps, TimeSeries, - TimeSeriesViewModel, + TimeSeriesVM, + TimeSeriesVMs, DataQuery, DataQueryResponse, DataQueryOptions, diff --git a/public/app/types/series.ts b/public/app/types/series.ts index dc1d1d7d393..9a906377636 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -28,10 +28,16 @@ export interface TimeSeries { unit?: string; } -export interface TimeSeriesViewModel { +/** View model projection of a time series */ +export interface TimeSeriesVM { label: string; color: string; - data: number[][]; + data: TimeSeriesValue[][]; +} + +/** View model projection of many time series */ +export interface TimeSeriesVMs { + [index: number]: TimeSeriesVM; } export interface DataQueryResponse { diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx index ed638e7bd16..38d29af8da9 100644 --- a/public/app/viz/Graph.tsx +++ b/public/app/viz/Graph.tsx @@ -6,8 +6,7 @@ import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; // Types -import TimeSeries from 'app/core/time_series2'; -import { TimeRange } from 'app/types'; +import { TimeRange, TimeSeriesVMs } from 'app/types'; // Copied from graph.ts function time_format(ticks, min, max) { @@ -60,7 +59,7 @@ const FLOT_OPTIONS = { }; interface GraphProps { - timeSeries: TimeSeries[]; + timeSeries: TimeSeriesVMs; timeRange: TimeRange; size?: { width: number; height: number }; } @@ -85,11 +84,9 @@ export class Graph extends PureComponent { draw() { const { size, timeSeries, timeRange } = this.props; - const data = timeSeries.map((ts: TimeSeries) => ({ - color: ts.color, - label: ts.label, - data: ts.getFlotPairs('null'), - })); + if (!size) { + return; + } const ticks = (size.width || 0) / 100; const min = timeRange.from.valueOf(); @@ -111,7 +108,7 @@ export class Graph extends PureComponent { ...dynamicOptions, }; - $.plot(this.element, data, options); + $.plot(this.element, timeSeries, options); } render() { diff --git a/public/app/viz/state/getTimeSeriesViewModel.ts b/public/app/viz/state/getTimeSeriesViewModel.ts deleted file mode 100644 index 9ad2216d135..00000000000 --- a/public/app/viz/state/getTimeSeriesViewModel.ts +++ /dev/null @@ -1,9 +0,0 @@ -import colors from 'app/core/utils/colors'; -import { TimeSeries, TimeSeriesViewModel } from 'app/types'; - -interface Options { - ts: TimeSeries; - seriesIndex: number; -} - -export function getTimeSeriesViewModel(ts: TimeSeries): TimeSeriesViewModel {} diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts new file mode 100644 index 00000000000..7658a2b2120 --- /dev/null +++ b/public/app/viz/state/timeSeries.ts @@ -0,0 +1,21 @@ +import colors from 'app/core/utils/colors'; +import { TimeSeries, TimeSeriesVMs } from 'app/types'; + +interface Options { + timeSeries: TimeSeries[]; +} + +export function getTimeSeriesVMs({ timeSeries }: Options): TimeSeriesVMs { + const vmSeries = timeSeries.map((item, index) => { + const colorIndex = index % colors.length; + const label = item.target; + + return { + data: item.datapoints, + label: label, + color: colors[colorIndex], + }; + }); + + return vmSeries; +} From ae7a1bc1392c65a641b0820a1248b1b0f49d846c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 15 Oct 2018 21:52:24 +0200 Subject: [PATCH 050/118] Can render graph --- .../features/dashboard/dashgrid/DataPanel.tsx | 6 +- public/app/plugins/panel/graph/graph.ts | 1 + public/app/plugins/panel/graph2/module.tsx | 13 +- public/app/types/index.ts | 4 + public/app/types/series.ts | 28 ++++ public/app/viz/Graph.tsx | 1 + public/app/viz/state/timeSeries.ts | 153 +++++++++++++++++- 7 files changed, 194 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 8014e06f43f..85821c19742 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Types -import { TimeRange, LoadingState, DataQueryResponse, TimeSeries } from 'app/types'; +import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types'; interface RenderProps { loading: LoadingState; @@ -82,14 +82,14 @@ export class DataPanel extends Component { const dataSourceSrv = getDatasourceSrv(); const ds = await dataSourceSrv.get(datasource); - const queryOptions = { + const queryOptions: DataQueryOptions = { timezone: 'browser', panelId: panelId, dashboardId: dashboardId, range: timeRange, rangeRaw: timeRange.raw, interval: '1s', - intervalMs: 1000, + intervalMs: 60000, targets: queries, maxDataPoints: 500, scopedVars: {}, diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 7a8e24539f7..bee9b9ec9b0 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -339,6 +339,7 @@ class GraphElement { callPlot(options, incrementRenderCounter) { try { + console.log('plot', this.sortedSeries); this.plot = $.plot(this.elem, this.sortedSeries, options); if (this.ctrl.renderError) { delete this.ctrl.error; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index e8d594e7968..c2b8c355440 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -4,12 +4,10 @@ import React, { PureComponent } from 'react'; // Components import Graph from 'app/viz/Graph'; - -// Utils import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; // Types -import { PanelProps } from 'app/types'; +import { PanelProps, NullValueMode } from 'app/types'; interface Options { showBars: boolean; @@ -26,10 +24,13 @@ export class Graph2 extends PureComponent { render() { const { timeSeries, timeRange } = this.props; - const viewModels = getTimeSeriesVMs({ timeSeries }); - console.log(viewModels); - return ; + const vmSeries = getTimeSeriesVMs({ + timeSeries: timeSeries, + nullValueMode: NullValueMode.Ignore, + }); + + return ; } } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index d274c99253c..b45b5f3e320 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -15,6 +15,8 @@ import { TimeSeries, TimeSeriesVM, TimeSeriesVMs, + TimeSeriesStats, + NullValueMode, DataQuery, DataQueryResponse, DataQueryOptions, @@ -62,6 +64,8 @@ export { TimeSeries, TimeSeriesVM, TimeSeriesVMs, + NullValueMode, + TimeSeriesStats, DataQuery, DataQueryResponse, DataQueryOptions, diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 9a906377636..5396880611b 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -33,6 +33,30 @@ export interface TimeSeriesVM { label: string; color: string; data: TimeSeriesValue[][]; + stats: TimeSeriesStats; +} + +export interface TimeSeriesStats { + total: number; + max: number; + min: number; + logmin: number; + avg: number | null; + current: number | null; + first: number | null; + delta: number; + diff: number | null; + range: number | null; + timeStep: number; + count: number; + allIsNull: boolean; + allIsZero: boolean; +} + +export enum NullValueMode { + Null = 'null', + Ignore = 'connected', + AsZero = 'null as zero', } /** View model projection of many time series */ @@ -56,6 +80,10 @@ export interface DataQueryOptions { panelId: number; dashboardId: number; cacheTimeout?: string; + interval: string; + intervalMs: number; + maxDataPoints: number; + scopedVars: object; } export interface DataSourceApi { diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx index 38d29af8da9..fab65225715 100644 --- a/public/app/viz/Graph.tsx +++ b/public/app/viz/Graph.tsx @@ -108,6 +108,7 @@ export class Graph extends PureComponent { ...dynamicOptions, }; + console.log('plot', timeSeries, options); $.plot(this.element, timeSeries, options); } diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts index 7658a2b2120..e22cb4681b7 100644 --- a/public/app/viz/state/timeSeries.ts +++ b/public/app/viz/state/timeSeries.ts @@ -1,19 +1,166 @@ +// Libraries +import _ from 'lodash'; + +// Utils import colors from 'app/core/utils/colors'; -import { TimeSeries, TimeSeriesVMs } from 'app/types'; + +// Types +import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types'; interface Options { timeSeries: TimeSeries[]; + nullValueMode: NullValueMode; } -export function getTimeSeriesVMs({ timeSeries }: Options): TimeSeriesVMs { +export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = timeSeries.map((item, index) => { const colorIndex = index % colors.length; const label = item.target; + const result = []; + + // stat defaults + let total = 0; + let max = -Number.MAX_VALUE; + let min = Number.MAX_VALUE; + let logmin = Number.MAX_VALUE; + let avg = null; + let current = null; + let first = null; + let delta = 0; + let diff = null; + let range = null; + let timeStep = Number.MAX_VALUE; + let allIsNull = true; + let allIsZero = true; + + const ignoreNulls = nullValueMode === NullValueMode.Ignore; + const nullAsZero = nullValueMode === NullValueMode.AsZero; + + let currentTime; + let currentValue; + let nonNulls = 0; + let previousTime; + let previousValue = 0; + let previousDeltaUp = true; + + for (let i = 0; i < item.datapoints.length; i++) { + currentValue = item.datapoints[i][0]; + currentTime = item.datapoints[i][1]; + + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== undefined) { + const currentStep = currentTime - previousTime; + if (currentStep < timeStep) { + timeStep = currentStep; + } + } + + previousTime = currentTime; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (currentValue !== null) { + if (_.isNumber(currentValue)) { + total += currentValue; + allIsNull = false; + nonNulls++; + } + + if (currentValue > max) { + max = currentValue; + } + + if (currentValue < min) { + min = currentValue; + } + + if (first === null) { + first = currentValue; + } else { + if (previousValue > currentValue) { + // counter reset + previousDeltaUp = false; + if (i === item.datapoints.length - 1) { + // reset on last + delta += currentValue; + } + } else { + if (previousDeltaUp) { + delta += currentValue - previousValue; // normal increment + } else { + delta += currentValue; // account for counter reset + } + previousDeltaUp = true; + } + } + previousValue = currentValue; + + if (currentValue < logmin && currentValue > 0) { + logmin = currentValue; + } + + if (currentValue !== 0) { + allIsZero = false; + } + } + + result.push([currentTime, currentValue]); + } + + if (max === -Number.MAX_VALUE) { + max = null; + } + + if (min === Number.MAX_VALUE) { + min = null; + } + + if (result.length && !allIsNull) { + avg = total / nonNulls; + current = result[result.length - 1][1]; + if (current === null && result.length > 1) { + current = result[result.length - 2][1]; + } + } + + if (max !== null && min !== null) { + range = max - min; + } + + if (current !== null && first !== null) { + diff = current - first; + } + + const count = result.length; return { - data: item.datapoints, + data: result, label: label, color: colors[colorIndex], + stats: { + total, + min, + max, + current, + logmin, + avg, + diff, + delta, + timeStep, + range, + count, + first, + allIsZero, + allIsNull, + }, }; }); From 287ba77abff0c9ba46fea36b7dfd883181022fd8 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 18 Oct 2018 14:28:41 +0900 Subject: [PATCH 051/118] don't overwrite unit if user set --- public/app/plugins/panel/graph/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index bc0d0c0c630..0701f1ddbbb 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -238,7 +238,7 @@ class GraphCtrl extends MetricsPanelCtrl { for (const series of this.seriesList) { series.applySeriesOverrides(this.panel.seriesOverrides); - if (series.unit) { + if (this.panel.yaxes[series.yaxis - 1].format === 'none' && series.unit) { this.panel.yaxes[series.yaxis - 1].format = series.unit; } } From e465b2d53ab59f8a004db580fdd3902d76536878 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 18 Oct 2018 20:45:03 +0900 Subject: [PATCH 052/118] Revert "don't overwrite unit if user set" This reverts commit 9dd33b79e037fc75ddc5f3a6b294edba99e99b94. --- public/app/plugins/panel/graph/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 0701f1ddbbb..bc0d0c0c630 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -238,7 +238,7 @@ class GraphCtrl extends MetricsPanelCtrl { for (const series of this.seriesList) { series.applySeriesOverrides(this.panel.seriesOverrides); - if (this.panel.yaxes[series.yaxis - 1].format === 'none' && series.unit) { + if (series.unit) { this.panel.yaxes[series.yaxis - 1].format = series.unit; } } From 4687ce2f7b884fd87f23a6d11bb9d3d0d64dcdc6 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 18 Oct 2018 20:51:36 +0900 Subject: [PATCH 053/118] allow unit override if cloudwatch response unit is none --- public/app/plugins/datasource/cloudwatch/datasource.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 7821c5dcb2c..b4f739f934c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -137,7 +137,11 @@ export default class CloudWatchDatasource { if (res.results) { _.forEach(res.results, queryRes => { _.forEach(queryRes.series, series => { - data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' }); + const s = { target: series.name, datapoints: series.points } as any; + if (queryRes.meta.unit) { + s.unit = queryRes.meta.unit; + } + data.push(s); }); }); } From d1740f090a0a0479a9e4999d7225c0a5c4ba8faf Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 18 Oct 2018 16:42:08 +0200 Subject: [PATCH 054/118] stackdriver: make sure unit is not returned to the panel if mapping from stackdriver unit to grafana unit can't be made --- .../app/plugins/datasource/stackdriver/datasource.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7ea748e1082..581c4f5002c 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -89,7 +89,7 @@ export default class StackdriverDatasource { } resolvePanelUnitFromTargets(targets: any[]) { - let unit = 'none'; + let unit; if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) { if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) { unit = stackdriverUnitMappings[targets[0].unit]; @@ -109,13 +109,17 @@ export default class StackdriverDatasource { const unit = this.resolvePanelUnitFromTargets(options.targets); queryRes.series.forEach(series => { - result.push({ + let timeSerie = { target: series.name, datapoints: series.points, refId: queryRes.refId, meta: queryRes.meta, unit, - }); + }; + if (unit) { + timeSerie = { ...timeSerie, unit }; + } + result.push(timeSerie); }); }); } From 0f0763b6b8cc3036d7f19f185c3be1778bbfe607 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 18 Oct 2018 16:45:46 +0200 Subject: [PATCH 055/118] stackdriver: fix failing tests --- .../datasource/stackdriver/specs/datasource.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 3117be402a9..ab0c0653816 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -235,8 +235,8 @@ describe('StackdriverDataSource', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]); }); - it('should return none', () => { - expect(res).toEqual('none'); + it('should return undefined', () => { + expect(res).toBeUndefined(); }); }); describe('and the stackdriver unit has a corresponding grafana unit', () => { @@ -262,16 +262,16 @@ describe('StackdriverDataSource', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]); }); - it('should return the default value - none', () => { - expect(res).toEqual('none'); + it('should return the default value of undefined', () => { + expect(res).toBeUndefined(); }); }); describe('and all target units are not the same', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]); }); - it('should return the default value - none', () => { - expect(res).toEqual('none'); + it('should return the default value of undefined', () => { + expect(res).toBeUndefined(); }); }); }); From 11f66bb909ac5ee45a7b77581abc66264824a40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Oct 2018 09:43:54 +0200 Subject: [PATCH 056/118] fixed issue with template refresh --- public/app/features/dashboard/dashboard_ctrl.ts | 7 +++++++ public/app/features/dashboard/dashboard_model.ts | 4 ++++ .../features/dashboard/dashgrid/DashboardGrid.tsx | 1 - public/app/features/dashboard/time_srv.ts | 4 ++-- public/app/features/panel/metrics_panel_ctrl.ts | 1 - public/app/features/panel/panel_directive.ts | 1 - public/app/features/plugins/plugin_component.ts | 1 - public/app/features/templating/variable_srv.ts | 15 ++++++--------- .../app/plugins/datasource/testdata/datasource.ts | 1 - public/app/plugins/panel/graph/graph.ts | 1 - 10 files changed, 19 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 696a40ef71a..65421a01392 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -162,10 +162,17 @@ export class DashboardCtrl { this.dashboard.removePanel(panel); } + onDestroy() { + if (this.dashboard) { + this.dashboard.destroy(); + } + } + init(dashboard) { this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); + this.$scope.$on('$destroy', this.onDestroy.bind(this)); this.setupDashboard(dashboard); } } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index aba5be24e8b..65a234a2b94 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -200,6 +200,10 @@ export class DashboardModel { this.events.emit('view-mode-changed', panel); } + timeRangeUpdated() { + this.events.emit('time-range-updated'); + } + startRefresh() { this.events.emit('refresh'); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 4da929b981e..1f5fa4cbe12 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -172,7 +172,6 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; - console.log('render panels'); for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 06cadfa6530..03b4a408125 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -6,8 +6,8 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; - // Types + import { TimeRange } from 'app/types'; export class TimeSrv { @@ -147,7 +147,7 @@ export class TimeSrv { } refreshDashboard() { - this.dashboard.startRefresh(); + this.dashboard.timeRangeUpdated(); } private startNextRefreshTimer(afterMs) { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index aa25308e121..e517c48bb59 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -61,7 +61,6 @@ 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_directive.ts b/public/app/features/panel/panel_directive.ts index d4554a2eed9..77ebf754b3a 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -135,7 +135,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.events.on('panel-size-changed', () => { ctrl.calculatePanelHeight(); $timeout(() => { - console.log('panel directive panel size changed, render'); resizeScrollableContent(); ctrl.render(); }); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 4b421ca446a..142eb942a30 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -211,7 +211,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ elem.append(child); setTimeout(() => { scope.$applyAsync(() => { - console.log('post appendAndCompile, broadcast refresh', scope.panel); scope.$broadcast('component-did-mount'); }); }); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 89ff8dcfb9f..6139dbf6dad 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -13,13 +13,12 @@ export class VariableSrv { /** @ngInject */ constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) { - // update time variant variables - $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope); $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); } init(dashboard) { this.dashboard = dashboard; + this.dashboard.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); @@ -42,11 +41,7 @@ export class VariableSrv { }); } - onDashboardRefresh(evt, payload) { - if (payload && payload.fromVariableValueUpdated) { - return Promise.resolve({}); - } - + onTimeRangeUpdated() { const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => { const previousOptions = variable.options.slice(); @@ -57,7 +52,9 @@ export class VariableSrv { }); }); - return this.$q.all(promises); + return this.$q.all(promises).then(() => { + this.dashboard.startRefresh(); + }); } processVariable(variable, queryParams) { @@ -136,7 +133,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true }); + this.dashboard.startRefresh(); } }); } diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index d112e656f3f..0197626cd0b 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -62,7 +62,6 @@ class TestDataDatasource { }); } - console.log(res); return { data: data }; }); } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index bee9b9ec9b0..7a8e24539f7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -339,7 +339,6 @@ class GraphElement { callPlot(options, incrementRenderCounter) { try { - console.log('plot', this.sortedSeries); this.plot = $.plot(this.elem, this.sortedSeries, options); if (this.ctrl.renderError) { delete this.ctrl.error; From 936fe560801d9df64d9b9de698e33295cefabc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Oct 2018 10:05:48 +0200 Subject: [PATCH 057/118] fix: another set of fixes for refresh --- public/app/core/services/keybindingSrv.ts | 4 ++-- public/app/features/dashboard/dashboard_ctrl.ts | 4 ---- public/app/features/dashboard/settings/settings.ts | 2 +- public/app/features/dashboard/share_snapshot_ctrl.ts | 3 +-- public/app/features/dashboard/timepicker/settings.html | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index d8dfc958dd4..f43dc96cd37 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -148,7 +148,7 @@ export class KeybindingSrv { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('mod+s', e => { @@ -257,7 +257,7 @@ export class KeybindingSrv { }); this.bind('d r', () => { - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('d s', () => { diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 65421a01392..c34b9ddaff2 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -117,10 +117,6 @@ export class DashboardCtrl { return this.dashboard; } - timezoneChanged() { - this.$rootScope.$broadcast('refresh'); - } - getPanelContainer() { return this; } diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 048a51efead..b6a70ee4b98 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -32,7 +32,7 @@ export class SettingsCtrl { this.$scope.$on('$destroy', () => { this.dashboard.updateSubmenuVisibility(); - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); setTimeout(() => { this.$rootScope.appEvent('dash-scroll', { restore: true }); }); diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index ec487801948..ac09d63054d 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -46,8 +46,7 @@ export class ShareSnapshotCtrl { $scope.loading = true; $scope.snapshot.external = external; - - $rootScope.$broadcast('refresh'); + $scope.dashboard.startRefresh(); $timeout(() => { $scope.saveSnapshot(external); diff --git a/public/app/features/dashboard/timepicker/settings.html b/public/app/features/dashboard/timepicker/settings.html index 3cb8ca061fb..fd5170013c2 100644 --- a/public/app/features/dashboard/timepicker/settings.html +++ b/public/app/features/dashboard/timepicker/settings.html @@ -5,7 +5,7 @@
      - +
      From b2932058c7988e350d687fb60926e26519db41bb Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 19 Oct 2018 10:42:57 +0200 Subject: [PATCH 058/118] stackdriver: only add unit to resonse obj if it has a value --- public/app/plugins/datasource/stackdriver/datasource.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 581c4f5002c..b77abdbdab3 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -109,12 +109,11 @@ export default class StackdriverDatasource { const unit = this.resolvePanelUnitFromTargets(options.targets); queryRes.series.forEach(series => { - let timeSerie = { + let timeSerie: any = { target: series.name, datapoints: series.points, refId: queryRes.refId, meta: queryRes.meta, - unit, }; if (unit) { timeSerie = { ...timeSerie, unit }; From b95d64e7a0688e1cc82db7958afdb7bf9b17a41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Oct 2018 11:14:02 +0200 Subject: [PATCH 059/118] fix: fixed variable srv tests --- .../features/templating/specs/variable_srv.test.ts | 11 +++++++---- .../templating/specs/variable_srv_init.test.ts | 5 +++-- public/app/features/templating/variable_srv.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index 359d5b79a38..3df6ccb8b5b 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -1,5 +1,6 @@ import '../all'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import moment from 'moment'; import $q from 'q'; @@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) { return getVarMockConstructor(ctr, model, ctx); }; - ctx.variableSrv.init({ - templating: { list: [] }, - updateSubmenuVisibility: () => {}, - }); + ctx.variableSrv.init( + new DashboardModel({ + templating: { list: [] }, + updateSubmenuVisibility: () => {}, + }) + ); scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); ctx.variableSrv.addVariable(scenario.variable); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index b5d00a5289e..bda5b6aa577 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -2,6 +2,7 @@ import '../all'; import _ from 'lodash'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import $q from 'q'; describe('VariableSrv init', function(this: any) { @@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) { ctx.variableSrv.datasourceSrv = ctx.datasourceSrv; ctx.variableSrv.$location.search = () => scenario.urlParams; - ctx.variableSrv.dashboard = { + ctx.variableSrv.dashboard = new DashboardModel({ templating: { list: scenario.variables }, - }; + }); await ctx.variableSrv.init(ctx.variableSrv.dashboard); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 6139dbf6dad..a676f9c2848 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -18,7 +18,7 @@ export class VariableSrv { init(dashboard) { this.dashboard = dashboard; - this.dashboard.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); + this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); From 5b8178cdaed5a4a78b5037d2e762e0279d1e6080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Oct 2018 11:32:36 +0200 Subject: [PATCH 060/118] fixed gofmt issue after go update --- pkg/services/sqlstore/alert_notification_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 92910fb41de..629a6292eb5 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -44,8 +44,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) { s := *query.Result cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, + Id: s.Id, + Version: s.Version, AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, } @@ -100,8 +100,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) { s := *query.Result s.Version = 1000 cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.NotifierId, - Version: s.Version, + Id: s.NotifierId, + Version: s.Version, AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, } err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) @@ -111,8 +111,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() { s := *query.Result cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, + Id: s.Id, + Version: s.Version, AlertRuleStateUpdatedVersion: 1000, } err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) @@ -125,8 +125,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) { s := *query.Result s.Version = 1000 cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, + Id: s.Id, + Version: s.Version, AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, } err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) From 4e6d59d4f4ade664766706e48c28d021a7a8486a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 19 Oct 2018 11:39:18 +0200 Subject: [PATCH 061/118] changelog: add notes about closing #13718 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c80f2852f2c..f9132195611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) +* **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) # 5.3.1 (2018-10-16) From eabb04cec21dc323347da1aab7fcbf2a6e9dd121 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 19 Oct 2018 13:27:21 +0200 Subject: [PATCH 062/118] changelog: add notes about closing #13667 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9132195611..db5457147f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ # 5.3.2 (unreleased) +* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm) * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) From 93a9885d673f7d57ab1b252cccc152dc97f726a7 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 19 Oct 2018 15:15:45 +0200 Subject: [PATCH 063/118] stackdriver: add default project to provisioning documentation --- docs/sources/features/datasources/stackdriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index cea86e96faf..103a58587b1 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -206,6 +206,7 @@ datasources: jsonData: tokenUri: https://oauth2.googleapis.com/token clientEmail: stackdriver@myproject.iam.gserviceaccount.com + defaultProject: my-project-name secureJsonData: privateKey: | -----BEGIN PRIVATE KEY----- From 16a2b207cc087f1a5272a1835207d12efc3790b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 22 Oct 2018 09:53:40 +0200 Subject: [PATCH 064/118] fix: DataPanel isFirstLoad state fix --- public/app/features/dashboard/dashgrid/DataPanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 85821c19742..d0122363668 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -103,10 +103,11 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Done, response: resp, + isFirstLoad: false, }); } catch (err) { console.log('Loading error', err); - this.setState({ loading: LoadingState.Error }); + this.setState({ loading: LoadingState.Error, isFirstLoad: false }); } }; From 6c473d33f4a8bf18a0444edc32351b9c39f4a556 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 22 Oct 2018 14:33:19 +0200 Subject: [PATCH 065/118] Enterprise crosscompilation (#13783) * build: crosscompilation and release of enterprise. --- .circleci/config.yml | 90 +++++++++++++++++++++++++++-- scripts/build/build-all.sh | 15 ++++- scripts/build/build.sh | 10 +++- scripts/build/build_enterprise.sh | 58 ------------------- scripts/build/prepare-enterprise.sh | 6 ++ 5 files changed, 112 insertions(+), 67 deletions(-) delete mode 100755 scripts/build/build_enterprise.sh create mode 100755 scripts/build/prepare-enterprise.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index a5497e6c7e8..87cd502ef18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -238,8 +238,17 @@ jobs: steps: - checkout - run: - name: build, test and package grafana enterprise - command: './scripts/build/build_enterprise.sh' + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: checkout enterprise + command: './scripts/build/prepare_enterprise.sh' + - run: + name: test enterprise + command: 'go test ./pkg/extensions/...' + - run: + name: build and package enterprise + command: './scripts/build/build.sh -enterprise' - run: name: sign packages command: './scripts/build/sign_packages.sh' @@ -254,6 +263,53 @@ jobs: paths: - enterprise-dist/grafana-enterprise* + build-all-enterprise: + docker: + - image: grafana/build-container:1.2.0 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: checkout enterprise + command: './scripts/build/prepare_enterprise.sh' + - restore_cache: + key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }} + - run: + name: download phantomjs binaries + command: './scripts/build/download-phantomjs.sh' + - save_cache: + key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }} + paths: + - /tmp/phantomjs + - run: + name: test enterprise + command: 'go test ./pkg/extensions/...' + - run: + name: build and package grafana + command: './scripts/build/build-all.sh -enterprise' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: verify signed packages + command: | + mkdir -p ~/.rpmdb/pubkeys + curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key + ./scripts/build/verify_signed_packages.sh dist/*.rpm + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - run: + name: move enterprise packages into their own folder + command: 'mv dist enterprise-dist' + - persist_to_workspace: + root: . + paths: + - enterprise-dist/grafana-enterprise* + deploy-enterprise-master: docker: - image: circleci/python:2.7-stretch @@ -267,6 +323,19 @@ jobs: name: deploy to s3 command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master' +deploy-enterprise-release: + docker: + - image: circleci/python:2.7-stretch + steps: + - attach_workspace: + at: . + - run: + name: install awscli + command: 'sudo pip install awscli' + - run: + name: deploy to s3 + command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release' + deploy-master: docker: - image: circleci/python:2.7-stretch @@ -313,7 +382,7 @@ workflows: jobs: - build-all: filters: *filter-only-master - - build-enterprise: + - build-all-enterprise: filters: *filter-only-master - codespell: filters: *filter-only-master @@ -356,13 +425,15 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - - build-enterprise + - build-all-enterprise filters: *filter-only-master release: jobs: - build-all: filters: *filter-only-release + - build-all-enterprise: + filters: *filter-only-release - codespell: filters: *filter-only-release - gometalinter: @@ -385,6 +456,17 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-release + - deploy-enterprise-release: + requires: + - build-all + - build-all-enterprise + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-release - grafana-docker-release: requires: - build-all diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 0aaab2ce4a6..64e51ca6259 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -4,6 +4,10 @@ # This script is executed from within the container. # +set -e + +EXTRA_OPTS="$@" + CCARMV7=arm-linux-gnueabihf-gcc CCARM64=aarch64-linux-gnu-gcc CCOSX64=/tmp/osxcross/target/bin/o64-clang @@ -18,15 +22,20 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false" + OPT="-includeBuildNumber=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM}" + OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" fi +echo "Build arguments: $OPT" + go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build -go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build + +# MacOS build is broken atm. See Issue #13763 +#go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build + go run build.go -goos windows -cc ${CCWIN64} ${OPT} build CC=${CCX64} go run build.go ${OPT} build diff --git a/scripts/build/build.sh b/scripts/build/build.sh index d4c1c788b30..2cf9f5a8a21 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -4,6 +4,10 @@ # This script is executed from within the container. # +set -e + +EXTRA_OPTS="$@" + CCX64=/tmp/x86_64-centos6-linux-gnu/bin/x86_64-centos6-linux-gnu-gcc GOPATH=/go @@ -14,12 +18,14 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false" + OPT="-includeBuildNumber=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM}" + OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" fi +echo "Build arguments: $OPT" + CC=${CCX64} go run build.go ${OPT} build yarn install --pure-lockfile --no-progress diff --git a/scripts/build/build_enterprise.sh b/scripts/build/build_enterprise.sh deleted file mode 100755 index cda3952c36a..00000000000 --- a/scripts/build/build_enterprise.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# -# This script is executed from within the container. -# - -echo "building enterprise version" - -GOPATH=/go -REPO_PATH=$GOPATH/src/github.com/grafana/grafana - - -cd /go/src/github.com/grafana/grafana -echo "current dir: $(pwd)" - -cd .. -git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 10 -cd grafana-enterprise -#git checkout 7fbae9c1be3467c4a39cf6ad85278a6896ceb49f -./build.sh - -cd ../grafana - -function exit_if_fail { - command=$@ - echo "Executing '$command'" - eval $command - rc=$? - if [ $rc -ne 0 ]; then - echo "'$command' returned $rc." - exit $rc - fi -} - -exit_if_fail go test ./pkg/extensions/... - - -if [ "$CIRCLE_TAG" != "" ]; then - echo "Building a release from tag $ls" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false build -else - echo "Building incremental build for $CIRCLE_BRANCH" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true build -fi - -yarn install --pure-lockfile --no-progress - -source /etc/profile.d/rvm.sh - -echo "current dir: $(pwd)" - -if [ "$CIRCLE_TAG" != "" ]; then - echo "Packaging a release from tag $CIRCLE_TAG" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false package latest -else - echo "Packaging incremental build for $CIRCLE_BRANCH" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true package latest -fi diff --git a/scripts/build/prepare-enterprise.sh b/scripts/build/prepare-enterprise.sh new file mode 100755 index 00000000000..0e1c3da2dbd --- /dev/null +++ b/scripts/build/prepare-enterprise.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd .. +git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 1 +cd grafana-enterprise +./build.sh From 5b53c0497af412cfb93485255247937c2bafc300 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 2 Oct 2018 18:29:40 +0900 Subject: [PATCH 066/118] for development --- .../stackdriver/partials/config.html | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 46b79d8bb0d..71133d13c55 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -1,4 +1,4 @@ -
      +
      GCP Service Account

      @@ -24,7 +24,7 @@

      -
      +

      Service Account Authentication

      Upload your Service Account key file or paste in the contents of the file. The file @@ -81,6 +81,23 @@
      +

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

      + +
      +
      +

      Local machine

      + For development. +
      + +
      +
      + Default Project ID + +
      +
      +
      + +
      Do not forget to save your changes after uploading a file.
      From 022a45becd64816b00061cde40901271e90e5f9e Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 2 Oct 2018 18:30:53 +0900 Subject: [PATCH 067/118] Revert "for development" This reverts commit cf2f110e42bb0ca041be7bb0ebf549bf565c51bc. --- .../stackdriver/partials/config.html | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 71133d13c55..a236cb6832c 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -1,4 +1,4 @@ -
      +
      GCP Service Account

      @@ -24,7 +24,7 @@

      -
      +

      Service Account Authentication

      Upload your Service Account key file or paste in the contents of the file. The file @@ -83,20 +83,6 @@

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

      -
      -
      -

      Local machine

      - For development. -
      - -
      -
      - Default Project ID - -
      -
      -
      -
      Do not forget to save your changes after uploading a file. From 43aa6603a3ef4e7b60192518d32f26dd60a6411d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 2 Oct 2018 18:40:39 +0900 Subject: [PATCH 068/118] fetch token from GCE metadata server --- pkg/api/pluginproxy/ds_auth_provider.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index c68da839d13..0cf3489e718 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -12,6 +12,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" + "golang.org/x/oauth2/google" ) //ApplyRoute should use the plugin route data to set auth headers and custom headers @@ -61,6 +62,21 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) } } + + if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { + tokenSrc, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/monitoring.read") + if err != nil { + logger.Error("Failed to get access token", "error", err) + } else { + token, err := tokenSrc.Token() + if err != nil { + logger.Error("Failed to get access token", "error", err) + } else { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + } + } + } + logger.Info("Requesting", "url", req.URL.String()) } From fbe0ffd10b3e861e3023fa17575a4e421167b122 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 3 Oct 2018 12:29:40 +0900 Subject: [PATCH 069/118] skip jwt token auth if privateKey is empty --- pkg/api/pluginproxy/ds_auth_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 0cf3489e718..d32d34f6187 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,7 +55,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if route.JwtTokenAuth != nil { + if route.JwtTokenAuth != nil && len(ds.SecureJsonData["privateKey"]) != 0 { if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) } else { From df215d9d09caaab28cc736c5403b31b08f2ef276 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 3 Oct 2018 17:08:13 +0200 Subject: [PATCH 070/118] stackdriver: WIP - test retrieving project id from gce metadata --- pkg/api/pluginproxy/ds_auth_provider.go | 26 +++++++++++++++++++++++-- pkg/tsdb/stackdriver/stackdriver.go | 3 +++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index d32d34f6187..320c4cc2cd3 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -64,11 +64,33 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { - tokenSrc, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/monitoring.read") + defaultCredentials, err := google.FindDefaultCredentials(ctx) + projectName := ds.JsonData.Get("defaultProject").MustString() + if projectName == "" { + ds.JsonData.Set("defaultProject", defaultCredentials.ProjectID) + } + if err != nil { + logger.Error("Failed to get default credentials", "error", err) + } else { + token, err := defaultCredentials.TokenSource.Token() + if err != nil { + logger.Error("Failed to get default access token", "error", err) + } else { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + } + } + } + + if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { + credentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") + projectName := ds.JsonData.Get("defaultProject").MustString() + if projectName == "" { + ds.JsonData.Set("defaultProject", credentials.ProjectID) + } if err != nil { logger.Error("Failed to get access token", "error", err) } else { - token, err := tokenSrc.Token() + token, err := credentials.TokenSource.Token() if err != nil { logger.Error("Failed to get access token", "error", err) } else { diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 96242dfdec4..f555ebfa3f4 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" @@ -551,6 +553,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. return nil, errors.New("Unable to find datasource plugin Stackdriver") } projectName := dsInfo.JsonData.Get("defaultProject").MustString() + logger.Info("projectName", "projectName", projectName) proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries") var stackdriverRoute *plugins.AppPluginRoute From 200216207384f7633ab7ef8b54e2d6caa826f241 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 3 Oct 2018 21:37:00 +0200 Subject: [PATCH 071/118] stackdriver: wip - add scope and remove debug code --- pkg/api/pluginproxy/ds_auth_provider.go | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 320c4cc2cd3..85d6beb35e6 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -64,7 +64,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { - defaultCredentials, err := google.FindDefaultCredentials(ctx) + defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") projectName := ds.JsonData.Get("defaultProject").MustString() if projectName == "" { ds.JsonData.Set("defaultProject", defaultCredentials.ProjectID) @@ -81,24 +81,6 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { - credentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") - projectName := ds.JsonData.Get("defaultProject").MustString() - if projectName == "" { - ds.JsonData.Set("defaultProject", credentials.ProjectID) - } - if err != nil { - logger.Error("Failed to get access token", "error", err) - } else { - token, err := credentials.TokenSource.Token() - if err != nil { - logger.Error("Failed to get access token", "error", err) - } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) - } - } - } - logger.Info("Requesting", "url", req.URL.String()) } From b2de0d8c3e0ba10122083091830f2f30aa7090e6 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 3 Oct 2018 21:48:20 +0200 Subject: [PATCH 072/118] stackdriver: wip - always use gce default account for stackdriver --- pkg/api/pluginproxy/ds_auth_provider.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 85d6beb35e6..b551a9a4d8a 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,15 +55,15 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if route.JwtTokenAuth != nil && len(ds.SecureJsonData["privateKey"]) != 0 { - if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { - logger.Error("Failed to get access token", "error", err) - } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - } - } + // if route.JwtTokenAuth != nil && len(ds.SecureJsonData["privateKey"]) != 0 { + // if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { + // logger.Error("Failed to get access token", "error", err) + // } else { + // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + // } + // } - if req.Header.Get("Authorization") == "" && ds.Type == "stackdriver" { + if ds.Type == "stackdriver" { defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") projectName := ds.JsonData.Get("defaultProject").MustString() if projectName == "" { From 3f9ed2efaa7ff85cd622efabcb9990f06e961e0c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 08:39:58 +0200 Subject: [PATCH 073/118] stackdriver: wip - temp remove jwt token auth --- pkg/api/pluginproxy/ds_auth_provider.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index b551a9a4d8a..5a8bf5605dd 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -64,11 +64,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route // } if ds.Type == "stackdriver" { - defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") - projectName := ds.JsonData.Get("defaultProject").MustString() - if projectName == "" { - ds.JsonData.Set("defaultProject", defaultCredentials.ProjectID) - } + defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) if err != nil { logger.Error("Failed to get default credentials", "error", err) } else { @@ -76,6 +72,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if err != nil { logger.Error("Failed to get default access token", "error", err) } else { + ds.JsonData.Set("defaultProject", defaultCredentials.ProjectID) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) } } From 102f531c1e8e20dc2ef83b988ffe21ee33eee905 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 13:23:46 +0200 Subject: [PATCH 074/118] stackdriver: break out project name resolving into its own function in the stackdriver.go file --- pkg/tsdb/stackdriver/stackdriver.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index f555ebfa3f4..87856e3f36c 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "golang.org/x/net/context/ctxhttp" + "golang.org/x/oauth2/google" "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/components/null" @@ -518,6 +519,23 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte { return nil } +func getProjectName(ctx context.Context, dsInfo *models.DataSource, route *plugins.AppPluginRoute) (string, error) { + var projectName string + gceAutoAuthentication := dsInfo.JsonData.Get("gceAutoAuthentication").MustBool() + if gceAutoAuthentication { + defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) + if err != nil { + return "", err + } else { + projectName = defaultCredentials.ProjectID + } + } else { + projectName = dsInfo.JsonData.Get("defaultProject").MustString() + } + logger.Info("projectName", "projectName", projectName) + return projectName, nil +} + func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { bucketBound := "0" if n == 0 { @@ -552,9 +570,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. if !ok { return nil, errors.New("Unable to find datasource plugin Stackdriver") } - projectName := dsInfo.JsonData.Get("defaultProject").MustString() - logger.Info("projectName", "projectName", projectName) - proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries") var stackdriverRoute *plugins.AppPluginRoute for _, route := range plugin.Routes { @@ -564,6 +579,14 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. } } + // projectName := dsInfo.JsonData.Get("defaultProject").MustString() + // logger.Info("projectName", "projectName", projectName) + projectName, err := getProjectName(ctx, dsInfo, stackdriverRoute) + if err != nil { + return nil, err + } + proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries") + pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) return req, nil From 77e4c4249dbe42b2d73e21e49bf17311f353525d Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 13:48:08 +0200 Subject: [PATCH 075/118] stackdriver: wip - add very basic checkbox for gce auto authentication --- .../plugins/datasource/stackdriver/partials/config.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index a236cb6832c..6fd52a898f5 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -31,6 +31,9 @@ contents will be encrypted and saved in the Grafana database.
      + +
      @@ -60,9 +63,9 @@
      - Client Email - -
      + Client Email + +
      Token URI From 60617d0bf714aff4ecf75bc0d08581db50c78e4f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 8 Oct 2018 13:49:27 +0200 Subject: [PATCH 076/118] stackdriver: wip - add logic for retrieving token from gce metadata server in the auth provider --- pkg/api/pluginproxy/ds_auth_provider.go | 22 +++++++++++----------- pkg/tsdb/stackdriver/stackdriver.go | 5 +++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 5a8bf5605dd..8c6646f42b5 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,24 +55,24 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - // if route.JwtTokenAuth != nil && len(ds.SecureJsonData["privateKey"]) != 0 { - // if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { - // logger.Error("Failed to get access token", "error", err) - // } else { - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - // } - // } + gceAutoAuthentication := ds.JsonData.Get("gceAutomaticAuthentication").MustBool() + if route.JwtTokenAuth != nil && !gceAutoAuthentication { + if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { + logger.Error("Failed to get access token", "error", err) + } else { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + } + } - if ds.Type == "stackdriver" { - defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) + if gceAutoAuthentication { + tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) if err != nil { logger.Error("Failed to get default credentials", "error", err) } else { - token, err := defaultCredentials.TokenSource.Token() + token, err := tokenSrc.Token() if err != nil { logger.Error("Failed to get default access token", "error", err) } else { - ds.JsonData.Set("defaultProject", defaultCredentials.ProjectID) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) } } diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 87856e3f36c..dc7fbdc135e 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -521,8 +521,9 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte { func getProjectName(ctx context.Context, dsInfo *models.DataSource, route *plugins.AppPluginRoute) (string, error) { var projectName string - gceAutoAuthentication := dsInfo.JsonData.Get("gceAutoAuthentication").MustBool() - if gceAutoAuthentication { + gceAutomaticAuthentication := dsInfo.JsonData.Get("gceAutomaticAuthentication").MustBool() + logger.Info("gceAutomaticAuthentication", "gceAutomaticAuthentication", gceAutomaticAuthentication) + if gceAutomaticAuthentication { defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) if err != nil { return "", err From ff5f281508979087af273b9bac2edaa891317a7b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 09:32:16 +0200 Subject: [PATCH 077/118] stackdriver: wip - get metric descriptors in the backend --- pkg/api/pluginproxy/ds_auth_provider.go | 11 +++-- .../stackdriver/metric_descriptors_query.go | 47 +++++++++++++++++++ pkg/tsdb/stackdriver/stackdriver.go | 11 +++-- 3 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 pkg/tsdb/stackdriver/metric_descriptors_query.go diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 8c6646f42b5..d3b628b9de4 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -12,7 +12,6 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" - "golang.org/x/oauth2/google" ) //ApplyRoute should use the plugin route data to set auth headers and custom headers @@ -56,6 +55,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } gceAutoAuthentication := ds.JsonData.Get("gceAutomaticAuthentication").MustBool() + logger.Info("gceAutoAuthentication", "gceAutoAuthentication", gceAutoAuthentication) if route.JwtTokenAuth != nil && !gceAutoAuthentication { if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) @@ -65,21 +65,22 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } if gceAutoAuthentication { - tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) + // tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) if err != nil { logger.Error("Failed to get default credentials", "error", err) } else { - token, err := tokenSrc.Token() + // token, err := tokenSrc.Token() + token, err := tokenProvider.getJwtAccessToken(ctx, data) if err != nil { logger.Error("Failed to get default access token", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) } } } logger.Info("Requesting", "url", req.URL.String()) - } func interpolateString(text string, data templateData) (string, error) { diff --git a/pkg/tsdb/stackdriver/metric_descriptors_query.go b/pkg/tsdb/stackdriver/metric_descriptors_query.go new file mode 100644 index 00000000000..da2e5f5d9e0 --- /dev/null +++ b/pkg/tsdb/stackdriver/metric_descriptors_query.go @@ -0,0 +1,47 @@ +package stackdriver + +import ( + "context" + "fmt" + "io/ioutil" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/components/simplejson" + "golang.org/x/net/context/ctxhttp" + + "github.com/grafana/grafana/pkg/tsdb" +) + +func (e *StackdriverExecutor) executeMetricDescriptors(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + logger.Info("metricDescriptors", "metricDescriptors", tsdbQuery.Queries[0].RefId) + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} + result := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + + req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptors") + if err != nil { + slog.Error("Failed to create request", "error", err) + return nil, fmt.Errorf("Failed to create request. error: %v", err) + } + res, err := ctxhttp.Do(ctx, e.httpClient, req) + if err != nil { + logger.Info("error2", err) + return nil, err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + logger.Info("error3", err) + return nil, err + } + defer res.Body.Close() + if err != nil { + return nil, err + } + + queryResult.Meta.Set("test", string(body)) + logger.Info("string(body)", "string(body)", string(body)) + result.Results[tsdbQuery.Queries[0].RefId] = queryResult + + return result, nil +} diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index dc7fbdc135e..d36487e344a 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -74,6 +74,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour switch queryType { case "annotationQuery": result, err = e.executeAnnotationQuery(ctx, tsdbQuery) + case "metricDescriptors": + result, err = e.executeMetricDescriptors(ctx, tsdbQuery) case "timeSeriesQuery": fallthrough default: @@ -268,7 +270,7 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { 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) + req, err := e.createRequest(ctx, e.dsInfo, "timeSeries") if err != nil { queryResult.Error = err return queryResult, StackdriverResponse{}, nil @@ -526,7 +528,8 @@ func getProjectName(ctx context.Context, dsInfo *models.DataSource, route *plugi if gceAutomaticAuthentication { defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) if err != nil { - return "", err + // return "", err + projectName = "raintank-dev" } else { projectName = defaultCredentials.ProjectID } @@ -553,7 +556,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { return bucketBound } -func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { +func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, endpointName string)) (*http.Request, error) { u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "render") @@ -586,7 +589,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. if err != nil { return nil, err } - proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries") + proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/"+endpointName) pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) From 412a1f6d998a684b2d106167a8addea69883d210 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 13:17:29 +0200 Subject: [PATCH 078/118] stackdriver: wip - return metric descriptors in the format of tablew --- .../stackdriver/metric_descriptors_query.go | 56 ++++++++++++-- pkg/tsdb/stackdriver/types.go | 21 ++++++ .../datasource/stackdriver/datasource.ts | 75 +++++++++++-------- 3 files changed, 114 insertions(+), 38 deletions(-) diff --git a/pkg/tsdb/stackdriver/metric_descriptors_query.go b/pkg/tsdb/stackdriver/metric_descriptors_query.go index da2e5f5d9e0..6c005d2966b 100644 --- a/pkg/tsdb/stackdriver/metric_descriptors_query.go +++ b/pkg/tsdb/stackdriver/metric_descriptors_query.go @@ -2,8 +2,11 @@ package stackdriver import ( "context" + "encoding/json" "fmt" "io/ioutil" + "net/http" + "strings" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/simplejson" @@ -29,19 +32,56 @@ func (e *StackdriverExecutor) executeMetricDescriptors(ctx context.Context, tsdb logger.Info("error2", err) return nil, err } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - logger.Info("error3", err) - return nil, err - } - defer res.Body.Close() + + data, err := e.unmarshalMetricDescriptors(res) if err != nil { return nil, err } - queryResult.Meta.Set("test", string(body)) - logger.Info("string(body)", "string(body)", string(body)) + parts := strings.Split(req.URL.Path, "/") + defaultProject := parts[3] + + table := transformMetricDescriptorResponseToTable(data) + queryResult.Tables = append(queryResult.Tables, table) result.Results[tsdbQuery.Queries[0].RefId] = queryResult + result.Results[tsdbQuery.Queries[0].RefId].Meta.Set("defaultProject", defaultProject) return result, nil } + +func transformMetricDescriptorResponseToTable(data MetricDescriptorsResponse) *tsdb.Table { + table := &tsdb.Table{ + Columns: make([]tsdb.TableColumn, 1), + Rows: make([]tsdb.RowValues, 0), + } + table.Columns[0].Text = "metricDescriptor" + + for _, r := range data.MetricDescriptors { + values := make([]interface{}, 1) + values[0] = r + table.Rows = append(table.Rows, values) + } + return table +} + +func (e *StackdriverExecutor) unmarshalMetricDescriptors(res *http.Response) (MetricDescriptorsResponse, error) { + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return MetricDescriptorsResponse{}, err + } + + if res.StatusCode/100 != 2 { + slog.Error("Request failed", "status", res.Status, "body", string(body)) + return MetricDescriptorsResponse{}, fmt.Errorf(string(body)) + } + + var data MetricDescriptorsResponse + err = json.Unmarshal(body, &data) + if err != nil { + slog.Error("Failed to unmarshal MetricDescriptorResponse", "error", err, "status", res.Status, "body", string(body)) + return MetricDescriptorsResponse{}, err + } + + return data, nil +} diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index 3821ce7ceda..de6daa5dc06 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -73,3 +73,24 @@ type StackdriverResponse struct { } `json:"points"` } `json:"timeSeries"` } + +type MetricDescriptorsResponse struct { + MetricDescriptors []struct { + Name string `json:"name"` + Labels []struct { + Key string `json:"key"` + Description string `json:"description"` + } `json:"labels,omitempty"` + MetricKind string `json:"metricKind"` + ValueType string `json:"valueType"` + Unit string `json:"unit,omitempty"` + Description string `json:"description"` + DisplayName string `json:"displayName"` + Type string `json:"type"` + Metadata struct { + LaunchStage string `json:"launchStage"` + SamplePeriod string `json:"samplePeriod"` + IngestDelay string `json:"ingestDelay"` + } `json:"metadata"` + } `json:"metricDescriptors"` +} diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index b77abdbdab3..dae9a55bea1 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -173,38 +173,53 @@ export default class StackdriverDatasource { throw new Error('Template variables support is not yet imlemented'); } - testDatasource() { - const path = `v3/projects/${this.projectName}/metricDescriptors`; - return this.doRequest(`${this.baseUrl}${path}`) - .then(response => { - if (response.status === 200) { - return { - status: 'success', - message: 'Successfully queried the Stackdriver API.', - title: 'Success', - }; - } + async testDatasource() { + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [ + { + refId: 'metricDescriptors', + datasourceId: this.id, + type: 'metricDescriptors', + }, + ], + }, + }); + console.log(data); + return data; + // const path = `v3/projects/${this.projectName}/metricDescriptors`; + // return this.doRequest(`${this.baseUrl}${path}`) + // .then(response => { + // if (response.status === 200) { + // return { + // status: 'success', + // message: 'Successfully queried the Stackdriver API.', + // title: 'Success', + // }; + // } - return { - status: 'error', - message: 'Returned http status code ' + response.status, - }; - }) - .catch(error => { - let message = 'Stackdriver: '; - message += error.statusText ? error.statusText + ': ' : ''; + // return { + // status: 'error', + // message: 'Returned http status code ' + response.status, + // }; + // }) + // .catch(error => { + // let message = 'Stackdriver: '; + // message += error.statusText ? error.statusText + ': ' : ''; - if (error.data && error.data.error && error.data.error.code) { - // 400, 401 - message += error.data.error.code + '. ' + error.data.error.message; - } else { - message += 'Cannot connect to Stackdriver API'; - } - return { - status: 'error', - message: message, - }; - }); + // if (error.data && error.data.error && error.data.error.code) { + // // 400, 401 + // message += error.data.error.code + '. ' + error.data.error.message; + // } else { + // message += 'Cannot connect to Stackdriver API'; + // } + // return { + // status: 'error', + // message: message, + // }; + // }); } async getProjects() { From 9c7022be86197107b4a070f7f7da627e178e0883 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 13:35:50 +0200 Subject: [PATCH 079/118] stackdriver: add status code --- pkg/tsdb/stackdriver/metric_descriptors_query.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/stackdriver/metric_descriptors_query.go b/pkg/tsdb/stackdriver/metric_descriptors_query.go index 6c005d2966b..7cf4f2b97d2 100644 --- a/pkg/tsdb/stackdriver/metric_descriptors_query.go +++ b/pkg/tsdb/stackdriver/metric_descriptors_query.go @@ -22,7 +22,7 @@ func (e *StackdriverExecutor) executeMetricDescriptors(ctx context.Context, tsdb Results: make(map[string]*tsdb.QueryResult), } - req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptors") + req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptorss") if err != nil { slog.Error("Failed to create request", "error", err) return nil, fmt.Errorf("Failed to create request. error: %v", err) @@ -35,7 +35,11 @@ func (e *StackdriverExecutor) executeMetricDescriptors(ctx context.Context, tsdb data, err := e.unmarshalMetricDescriptors(res) if err != nil { - return nil, err + queryResult.ErrorString = fmt.Sprintf(`Status code: %d`, res.StatusCode) + logger.Info("error2", "ErrorString", queryResult.ErrorString) + queryResult.Error = err + result.Results[tsdbQuery.Queries[0].RefId] = queryResult + return result, nil } parts := strings.Split(req.URL.Path, "/") @@ -73,7 +77,7 @@ func (e *StackdriverExecutor) unmarshalMetricDescriptors(res *http.Response) (Me if res.StatusCode/100 != 2 { slog.Error("Request failed", "status", res.Status, "body", string(body)) - return MetricDescriptorsResponse{}, fmt.Errorf(string(body)) + return MetricDescriptorsResponse{}, fmt.Errorf(`Status code: %d - %s`, res.StatusCode, string(body)) } var data MetricDescriptorsResponse From dd9e9ed4191a5695eae4fcf38242eb360612ed83 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 15:20:05 +0200 Subject: [PATCH 080/118] stackdriver: make backend query a pure test datasource function --- .../stackdriver/metric_descriptors_query.go | 91 ------------------- pkg/tsdb/stackdriver/stackdriver.go | 31 +------ .../datasource/stackdriver/datasource.ts | 83 ++++++++--------- 3 files changed, 40 insertions(+), 165 deletions(-) delete mode 100644 pkg/tsdb/stackdriver/metric_descriptors_query.go diff --git a/pkg/tsdb/stackdriver/metric_descriptors_query.go b/pkg/tsdb/stackdriver/metric_descriptors_query.go deleted file mode 100644 index 7cf4f2b97d2..00000000000 --- a/pkg/tsdb/stackdriver/metric_descriptors_query.go +++ /dev/null @@ -1,91 +0,0 @@ -package stackdriver - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "github.com/grafana/grafana/pkg/components/simplejson" - "golang.org/x/net/context/ctxhttp" - - "github.com/grafana/grafana/pkg/tsdb" -) - -func (e *StackdriverExecutor) executeMetricDescriptors(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - logger.Info("metricDescriptors", "metricDescriptors", tsdbQuery.Queries[0].RefId) - queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} - result := &tsdb.Response{ - Results: make(map[string]*tsdb.QueryResult), - } - - req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptorss") - if err != nil { - slog.Error("Failed to create request", "error", err) - return nil, fmt.Errorf("Failed to create request. error: %v", err) - } - res, err := ctxhttp.Do(ctx, e.httpClient, req) - if err != nil { - logger.Info("error2", err) - return nil, err - } - - data, err := e.unmarshalMetricDescriptors(res) - if err != nil { - queryResult.ErrorString = fmt.Sprintf(`Status code: %d`, res.StatusCode) - logger.Info("error2", "ErrorString", queryResult.ErrorString) - queryResult.Error = err - result.Results[tsdbQuery.Queries[0].RefId] = queryResult - return result, nil - } - - parts := strings.Split(req.URL.Path, "/") - defaultProject := parts[3] - - table := transformMetricDescriptorResponseToTable(data) - queryResult.Tables = append(queryResult.Tables, table) - result.Results[tsdbQuery.Queries[0].RefId] = queryResult - result.Results[tsdbQuery.Queries[0].RefId].Meta.Set("defaultProject", defaultProject) - - return result, nil -} - -func transformMetricDescriptorResponseToTable(data MetricDescriptorsResponse) *tsdb.Table { - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, 1), - Rows: make([]tsdb.RowValues, 0), - } - table.Columns[0].Text = "metricDescriptor" - - for _, r := range data.MetricDescriptors { - values := make([]interface{}, 1) - values[0] = r - table.Rows = append(table.Rows, values) - } - return table -} - -func (e *StackdriverExecutor) unmarshalMetricDescriptors(res *http.Response) (MetricDescriptorsResponse, error) { - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return MetricDescriptorsResponse{}, err - } - - if res.StatusCode/100 != 2 { - slog.Error("Request failed", "status", res.Status, "body", string(body)) - return MetricDescriptorsResponse{}, fmt.Errorf(`Status code: %d - %s`, res.StatusCode, string(body)) - } - - var data MetricDescriptorsResponse - err = json.Unmarshal(body, &data) - if err != nil { - slog.Error("Failed to unmarshal MetricDescriptorResponse", "error", err, "status", res.Status, "body", string(body)) - return MetricDescriptorsResponse{}, err - } - - return data, nil -} diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index d36487e344a..2674e3a9902 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -15,10 +15,7 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "golang.org/x/net/context/ctxhttp" - "golang.org/x/oauth2/google" "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/components/null" @@ -75,7 +72,7 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour case "annotationQuery": result, err = e.executeAnnotationQuery(ctx, tsdbQuery) case "metricDescriptors": - result, err = e.executeMetricDescriptors(ctx, tsdbQuery) + result, err = e.executeTestDataSource(ctx, tsdbQuery) case "timeSeriesQuery": fallthrough default: @@ -521,25 +518,6 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte { return nil } -func getProjectName(ctx context.Context, dsInfo *models.DataSource, route *plugins.AppPluginRoute) (string, error) { - var projectName string - gceAutomaticAuthentication := dsInfo.JsonData.Get("gceAutomaticAuthentication").MustBool() - logger.Info("gceAutomaticAuthentication", "gceAutomaticAuthentication", gceAutomaticAuthentication) - if gceAutomaticAuthentication { - defaultCredentials, err := google.FindDefaultCredentials(ctx, route.JwtTokenAuth.Scopes...) - if err != nil { - // return "", err - projectName = "raintank-dev" - } else { - projectName = defaultCredentials.ProjectID - } - } else { - projectName = dsInfo.JsonData.Get("defaultProject").MustString() - } - logger.Info("projectName", "projectName", projectName) - return projectName, nil -} - func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { bucketBound := "0" if n == 0 { @@ -583,12 +561,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. } } - // projectName := dsInfo.JsonData.Get("defaultProject").MustString() - // logger.Info("projectName", "projectName", projectName) - projectName, err := getProjectName(ctx, dsInfo, stackdriverRoute) - if err != nil { - return nil, err - } + projectName := dsInfo.JsonData.Get("defaultProject").MustString() proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/"+endpointName) pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index dae9a55bea1..c7ce91a0227 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -174,52 +174,45 @@ export default class StackdriverDatasource { } async testDatasource() { - const { data } = await this.backendSrv.datasourceRequest({ - url: '/api/tsdb/query', - method: 'POST', - data: { - queries: [ - { - refId: 'metricDescriptors', - datasourceId: this.id, - type: 'metricDescriptors', - }, - ], - }, - }); - console.log(data); - return data; - // const path = `v3/projects/${this.projectName}/metricDescriptors`; - // return this.doRequest(`${this.baseUrl}${path}`) - // .then(response => { - // if (response.status === 200) { - // return { - // status: 'success', - // message: 'Successfully queried the Stackdriver API.', - // title: 'Success', - // }; - // } + try { + await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [ + { + refId: 'metricDescriptors', + datasourceId: this.id, + type: 'metricDescriptors', + }, + ], + }, + }); + return { + status: 'success', + message: 'Successfully queried the Stackdriver API.', + title: 'Success', + }; + } catch (error) { + console.log(error.data.error); + let message = 'Stackdriver: '; + message += error.statusText ? error.statusText + ': ' : ''; - // return { - // status: 'error', - // message: 'Returned http status code ' + response.status, - // }; - // }) - // .catch(error => { - // let message = 'Stackdriver: '; - // message += error.statusText ? error.statusText + ': ' : ''; - - // if (error.data && error.data.error && error.data.error.code) { - // // 400, 401 - // message += error.data.error.code + '. ' + error.data.error.message; - // } else { - // message += 'Cannot connect to Stackdriver API'; - // } - // return { - // status: 'error', - // message: message, - // }; - // }); + if (error.data && error.data.error && error.data.error) { + try { + const res = JSON.parse(error.data.error); + message += res.error.code + '. ' + res.error.message; + } catch (err) { + message += error.data.error; + } + } else { + message += 'Cannot connect to Stackdriver API'; + } + return { + status: 'error', + message: message, + }; + } } async getProjects() { From 2b1709b8750dc63bdb90350c81439d9c4c77d2b8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 15:23:13 +0200 Subject: [PATCH 081/118] stackdriver: only get default token from metadata server when applying route --- pkg/api/pluginproxy/ds_auth_provider.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index d3b628b9de4..3b7c6ee5c0f 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -12,6 +12,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" + "golang.org/x/oauth2/google" ) //ApplyRoute should use the plugin route data to set auth headers and custom headers @@ -64,18 +65,16 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if gceAutoAuthentication { - // tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) + if gceAutoAuthentication && route.JwtTokenAuth == nil { + tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) if err != nil { - logger.Error("Failed to get default credentials", "error", err) + logger.Error("Failed to get default token from meta data server", "error", err) } else { - // token, err := tokenSrc.Token() - token, err := tokenProvider.getJwtAccessToken(ctx, data) + token, err := tokenSrc.Token() if err != nil { - logger.Error("Failed to get default access token", "error", err) + logger.Error("Failed to get default access token from meta data server", "error", err) } else { - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) } } } From 0b82fda49ce52845a2bd0ee027427d3a15dc2334 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 16:05:35 +0200 Subject: [PATCH 082/118] stackdriver: only get default token from metadata server when applying route --- pkg/tsdb/stackdriver/test_datasource_query.go | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 pkg/tsdb/stackdriver/test_datasource_query.go diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go new file mode 100644 index 00000000000..0bebea47c7a --- /dev/null +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -0,0 +1,105 @@ +package stackdriver + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/components/simplejson" + "golang.org/x/net/context/ctxhttp" + "golang.org/x/oauth2/google" + + "github.com/grafana/grafana/pkg/tsdb" +) + +func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + gceAutomaticAuthentication := e.dsInfo.JsonData.Get("gceAutomaticAuthentication").MustBool() + if gceAutomaticAuthentication { + defaultProject, err := e.getDefaultProject(ctx) + if err != nil { + return nil, err + } + e.dsInfo.JsonData.Set("defaultProject", defaultProject) + } + + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} + result := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + + req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptors") + if err != nil { + slog.Error("Failed to create request", "error", err) + return nil, fmt.Errorf("Failed to create request. error: %v", err) + } + res, err := ctxhttp.Do(ctx, e.httpClient, req) + if err != nil { + return nil, err + } + + data, err := e.unmarshalMetricDescriptors(res) + if err != nil { + return nil, err + } + + parts := strings.Split(req.URL.Path, "/") + defaultProject := parts[3] + + table := transformMetricDescriptorResponseToTable(data) + queryResult.Tables = append(queryResult.Tables, table) + result.Results[tsdbQuery.Queries[0].RefId] = queryResult + result.Results[tsdbQuery.Queries[0].RefId].Meta.Set("defaultProject", defaultProject) + + return result, nil +} + +func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) { + defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") + if err != nil { + return "", err + } else { + logger.Info("projectName", "projectName", defaultCredentials.ProjectID) + return defaultCredentials.ProjectID, nil + } +} + +func transformMetricDescriptorResponseToTable(data MetricDescriptorsResponse) *tsdb.Table { + table := &tsdb.Table{ + Columns: make([]tsdb.TableColumn, 1), + Rows: make([]tsdb.RowValues, 0), + } + table.Columns[0].Text = "metricDescriptor" + + for _, r := range data.MetricDescriptors { + values := make([]interface{}, 1) + values[0] = r + table.Rows = append(table.Rows, values) + } + return table +} + +func (e *StackdriverExecutor) unmarshalMetricDescriptors(res *http.Response) (MetricDescriptorsResponse, error) { + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return MetricDescriptorsResponse{}, err + } + + if res.StatusCode/100 != 2 { + slog.Error("Request failed", "status", res.Status, "body", string(body)) + return MetricDescriptorsResponse{}, fmt.Errorf(`%s`, string(body)) + } + + var data MetricDescriptorsResponse + err = json.Unmarshal(body, &data) + if err != nil { + slog.Error("Failed to unmarshal MetricDescriptorResponse", "error", err, "status", res.Status, "body", string(body)) + return MetricDescriptorsResponse{}, err + } + + return data, nil +} From aa2e64e1603dfc7487ac48d0eaba2c7f00ce10cd Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 16:06:59 +0200 Subject: [PATCH 083/118] stackdriver: improve ui for toggling authentication type --- .../datasource/stackdriver/config_ctrl.ts | 12 ++++++- .../stackdriver/partials/config.html | 31 +++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/config_ctrl.ts b/public/app/plugins/datasource/stackdriver/config_ctrl.ts index d3847562156..13dba26ba86 100644 --- a/public/app/plugins/datasource/stackdriver/config_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/config_ctrl.ts @@ -5,13 +5,23 @@ export class StackdriverConfigCtrl { jsonText: string; validationErrors: string[] = []; inputDataValid: boolean; + authenticationTypes: any[]; + defaultAuthenticationType: string; /** @ngInject */ constructor(datasourceSrv) { + this.defaultAuthenticationType = 'jwt'; this.datasourceSrv = datasourceSrv; this.current.jsonData = this.current.jsonData || {}; + this.current.jsonData.authenticationType = this.current.jsonData.authenticationType + ? this.current.jsonData.authenticationType + : this.defaultAuthenticationType; this.current.secureJsonData = this.current.secureJsonData || {}; this.current.secureJsonFields = this.current.secureJsonFields || {}; + this.authenticationTypes = [ + { key: this.defaultAuthenticationType, value: 'Google JWT File' }, + { key: 'gce', value: 'Use GCE default Authentication' }, + ]; } save(jwt) { @@ -67,7 +77,7 @@ export class StackdriverConfigCtrl { this.inputDataValid = false; this.jsonText = ''; - this.current.jsonData = {}; + this.current.jsonData = Object.assign({}, { authenticationType: this.current.jsonData.authenticationType }); this.current.secureJsonData = {}; this.current.secureJsonFields = {}; } diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 6fd52a898f5..0cc08a3e9d8 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -31,10 +31,16 @@ contents will be encrypted and saved in the Grafana database.
      - +
      +
      + Authentication Type +
      + +
      +
      +
      -
      +
      @@ -55,23 +61,23 @@
      -
      +
      Uploaded Key Details
      - Project + Project
      - Client Email + Client Email
      - Token URI + Token URI
      - Private Key + Private Key
      @@ -84,9 +90,8 @@
      -

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

      +

      Do not forget to - -

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

      + Verify GCE automatic authentication by clicking Save & Test

      From 823838a83918f4e47b9c3a2e09b3364188adccd1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 16:28:04 +0200 Subject: [PATCH 084/118] stackdriver: wip - remove debug code --- pkg/api/pluginproxy/ds_auth_provider.go | 9 +++++---- pkg/tsdb/stackdriver/test_datasource_query.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 3b7c6ee5c0f..194e4e8e964 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,9 +55,9 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - gceAutoAuthentication := ds.JsonData.Get("gceAutomaticAuthentication").MustBool() - logger.Info("gceAutoAuthentication", "gceAutoAuthentication", gceAutoAuthentication) - if route.JwtTokenAuth != nil && !gceAutoAuthentication { + authenticationType := ds.JsonData.Get("authenticationType").MustString() + if route.JwtTokenAuth != nil && authenticationType == "jwt" { + logger.Info("authenticationType", "authenticationType", "jwt") if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) } else { @@ -65,7 +65,8 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if gceAutoAuthentication && route.JwtTokenAuth == nil { + if authenticationType == "gce" { + logger.Info("authenticationType", "authenticationType", "gce") tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) if err != nil { logger.Error("Failed to get default token from meta data server", "error", err) diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index 0bebea47c7a..822ca725f49 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -17,8 +17,8 @@ import ( ) func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - gceAutomaticAuthentication := e.dsInfo.JsonData.Get("gceAutomaticAuthentication").MustBool() - if gceAutomaticAuthentication { + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString() + if authenticationType == "gce" { defaultProject, err := e.getDefaultProject(ctx) if err != nil { return nil, err From cd7b454e25645e86a44ea26d3cea675707a75f50 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 16:36:45 +0200 Subject: [PATCH 085/118] stackdriver: add default value for authentication type --- pkg/api/pluginproxy/ds_auth_provider.go | 2 +- pkg/tsdb/stackdriver/test_datasource_query.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 194e4e8e964..60360733923 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,7 +55,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - authenticationType := ds.JsonData.Get("authenticationType").MustString() + authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt") if route.JwtTokenAuth != nil && authenticationType == "jwt" { logger.Info("authenticationType", "authenticationType", "jwt") if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index 822ca725f49..4ea79a79e43 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -17,7 +17,7 @@ import ( ) func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString() + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") if authenticationType == "gce" { defaultProject, err := e.getDefaultProject(ctx) if err != nil { From 9fb1d3a9a64077635c81abc2a11545395695a922 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 9 Oct 2018 17:02:41 +0200 Subject: [PATCH 086/118] stackdriver: only load project name if it's not already stored in the ds info json --- .../datasource/stackdriver/datasource.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index c7ce91a0227..91b2d19ba6d 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -222,12 +222,19 @@ export default class StackdriverDatasource { async getDefaultProject() { try { - const projects = await this.getProjects(); - if (projects && projects.length > 0) { - const test = projects.filter(p => p.id === this.projectName)[0]; - return test; + if (this.projectName) { + return { + id: this.projectName, + name: this.projectName, + }; } else { - throw new Error('No projects found'); + const projects = await this.getProjects(); + if (projects && projects.length > 0) { + const test = projects.filter(p => p.id === this.projectName)[0]; + return test; + } else { + throw new Error('No projects found'); + } } } catch (error) { let message = 'Projects cannot be fetched: '; From 835f1d788e1f767bb788a781f7c0bb98c1edcaae Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 11:01:02 +0200 Subject: [PATCH 087/118] stackdriver: display error message if project name is not present in jwt file --- public/app/plugins/datasource/stackdriver/config_ctrl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/stackdriver/config_ctrl.ts b/public/app/plugins/datasource/stackdriver/config_ctrl.ts index 13dba26ba86..9649a8772e9 100644 --- a/public/app/plugins/datasource/stackdriver/config_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/config_ctrl.ts @@ -45,6 +45,10 @@ export class StackdriverConfigCtrl { this.validationErrors.push('Client Email field missing in JWT file.'); } + if (!jwt.project_id || jwt.project_id.length === 0) { + this.validationErrors.push('Project Id field missing in JWT file.'); + } + if (this.validationErrors.length === 0) { this.inputDataValid = true; return true; From 09fb1760a1d7bd05c6ad2442701ddd9fc6ee0aed Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 11:04:06 +0200 Subject: [PATCH 088/118] stackdriver: only load default project from backend if it's not available on the target. this might happen when using gce authentication and provisioning --- .../datasource/stackdriver/datasource.ts | 78 ++++++++----------- .../stackdriver/partials/query.editor.html | 7 +- .../datasource/stackdriver/query_ctrl.ts | 10 +-- .../stackdriver/query_filter_ctrl.ts | 11 ++- 4 files changed, 46 insertions(+), 60 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 91b2d19ba6d..6e079a59fa3 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -6,6 +6,7 @@ export default class StackdriverDatasource { url: string; baseUrl: string; projectName: string; + queryPromise: Promise; /** @ngInject */ constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) { @@ -99,31 +100,34 @@ export default class StackdriverDatasource { } async query(options) { - const result = []; - const data = await this.getTimeSeries(options); - if (data.results) { - Object['values'](data.results).forEach(queryRes => { - if (!queryRes.series) { - return; - } - - const unit = this.resolvePanelUnitFromTargets(options.targets); - queryRes.series.forEach(series => { - let timeSerie: any = { - target: series.name, - datapoints: series.points, - refId: queryRes.refId, - meta: queryRes.meta, - }; - if (unit) { - timeSerie = { ...timeSerie, unit }; + this.queryPromise = new Promise(async resolve => { + const result = []; + const data = await this.getTimeSeries(options); + if (data.results) { + Object['values'](data.results).forEach(queryRes => { + if (!queryRes.series) { + return; } - result.push(timeSerie); + this.projectName = queryRes.meta.defaultProject; + const unit = this.resolvePanelUnitFromTargets(options.targets); + queryRes.series.forEach(series => { + let timeSerie: any = { + target: series.name, + datapoints: series.points, + refId: queryRes.refId, + meta: queryRes.meta, + }; + if (unit) { + timeSerie = { ...timeSerie, unit }; + } + result.push(timeSerie); + }); }); - }); - } + } - return { data: result }; + resolve({ data: result }); + }); + return this.queryPromise; } async annotationQuery(options) { @@ -181,9 +185,9 @@ export default class StackdriverDatasource { data: { queries: [ { - refId: 'metricDescriptors', + refId: 'testDatasource', datasourceId: this.id, - type: 'metricDescriptors', + type: 'testDatasource', }, ], }, @@ -215,27 +219,10 @@ export default class StackdriverDatasource { } } - async getProjects() { - const response = await this.doRequest(`/cloudresourcemanager/v1/projects`); - return response.data.projects.map(p => ({ id: p.projectId, name: p.name })); - } - async getDefaultProject() { try { - if (this.projectName) { - return { - id: this.projectName, - name: this.projectName, - }; - } else { - const projects = await this.getProjects(); - if (projects && projects.length > 0) { - const test = projects.filter(p => p.id === this.projectName)[0]; - return test; - } else { - throw new Error('No projects found'); - } - } + await this.queryPromise; + return this.projectName; } catch (error) { let message = 'Projects cannot be fetched: '; message += error.statusText ? error.statusText + ': ' : ''; @@ -252,12 +239,13 @@ export default class StackdriverDatasource { message += 'Cannot connect to Stackdriver API'; } appEvents.emit('ds-request-error', message); + return ''; } } - async getMetricTypes(projectId: string) { + async getMetricTypes(projectName: string) { try { - const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`; + const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`; const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); const metrics = data.metricDescriptors.map(m => { diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 7cc93e9d1a8..98c8fcc83e8 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -15,8 +15,7 @@
      Project - +
      -
      Alias Patterns
      +
      +
      Alias Patterns
      Format the legend keys any way you want by using alias patterns. Format the legend keys any way you want by using alias patterns.

      diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 8e1f24edeb7..0996ce82919 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -14,10 +14,7 @@ export interface QueryMeta { export class StackdriverQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; target: { - project: { - id: string; - name: string; - }; + defaultProject: string; unit: string; metricType: string; service: string; @@ -38,10 +35,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { defaultServiceValue = 'All Services'; defaults = { - project: { - id: 'default', - name: 'loading project...', - }, + defaultProject: 'loading project...', metricType: this.defaultDropdownValue, service: this.defaultServiceValue, metric: '', diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 7af76720d23..75e4817f395 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -79,12 +79,17 @@ export class StackdriverFilterCtrl { } async getCurrentProject() { - this.target.project = await this.datasource.getDefaultProject(); + return new Promise(async resolve => { + if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') { + this.target.defaultProject = await this.datasource.getDefaultProject(); + } + resolve(this.target.defaultProject); + }); } async loadMetricDescriptors() { - if (this.target.project.id !== 'default') { - this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); + if (this.target.defaultProject !== 'loading project...') { + this.metricDescriptors = await this.datasource.getMetricTypes(this.target.defaultProject); this.services = this.getServicesList(); this.metrics = this.getMetricsList(); return this.metricDescriptors; From b9cb4649bb073e5a6245a219a038e6bff9960dd9 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 11:55:45 +0200 Subject: [PATCH 089/118] stackdriver: get default project from backend. also ensure default project could be retrieved when authentication type is gce --- pkg/tsdb/stackdriver/default_project_query.go | 22 +++++++++++++ pkg/tsdb/stackdriver/stackdriver.go | 32 ++++++++++++++++++- pkg/tsdb/stackdriver/test_datasource_query.go | 25 ++++----------- .../datasource/stackdriver/datasource.ts | 21 ++++++++++-- 4 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 pkg/tsdb/stackdriver/default_project_query.go diff --git a/pkg/tsdb/stackdriver/default_project_query.go b/pkg/tsdb/stackdriver/default_project_query.go new file mode 100644 index 00000000000..16287fd09ff --- /dev/null +++ b/pkg/tsdb/stackdriver/default_project_query.go @@ -0,0 +1,22 @@ +package stackdriver + +import ( + "context" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" +) + +func (e *StackdriverExecutor) getGceDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} + result := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + defaultProject, err := e.getDefaultProject(ctx) + if err != nil { + return nil, err + } + queryResult.Meta.Set("defaultProject", defaultProject) + result.Results[tsdbQuery.Queries[0].RefId] = queryResult + return result, nil +} diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 2674e3a9902..0395a7ca554 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -16,8 +16,10 @@ import ( "time" "golang.org/x/net/context/ctxhttp" + "golang.org/x/oauth2/google" "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" @@ -71,8 +73,10 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour switch queryType { case "annotationQuery": result, err = e.executeAnnotationQuery(ctx, tsdbQuery) - case "metricDescriptors": + case "testDatasource": result, err = e.executeTestDataSource(ctx, tsdbQuery) + case "defaultProject": + result, err = e.getGceDefaultProject(ctx, tsdbQuery) case "timeSeriesQuery": fallthrough default: @@ -87,6 +91,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu Results: make(map[string]*tsdb.QueryResult), } + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") + if authenticationType == "gce" { + defaultProject, err := e.getDefaultProject(ctx) + if err != nil { + return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) + } + + e.dsInfo.JsonData.Set("defaultProject", defaultProject) + } + queries, err := e.buildQueries(tsdbQuery) if err != nil { return nil, err @@ -568,3 +582,19 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. return req, nil } + +func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) { + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") + if authenticationType == "gce" { + defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") + if err != nil { + return "raintank-production", nil + // return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) + } else { + logger.Info("projectName", "projectName", defaultCredentials.ProjectID) + return defaultCredentials.ProjectID, nil + } + } else { + return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil + } +} diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index 4ea79a79e43..7ed6cbb061d 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -8,27 +8,26 @@ import ( "net/http" "strings" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/simplejson" "golang.org/x/net/context/ctxhttp" - "golang.org/x/oauth2/google" "github.com/grafana/grafana/pkg/tsdb" ) func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} + result := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") if authenticationType == "gce" { defaultProject, err := e.getDefaultProject(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) } - e.dsInfo.JsonData.Set("defaultProject", defaultProject) - } - queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} - result := &tsdb.Response{ - Results: make(map[string]*tsdb.QueryResult), + e.dsInfo.JsonData.Set("defaultProject", defaultProject) } req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptors") @@ -57,16 +56,6 @@ func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQue return result, nil } -func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) { - defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") - if err != nil { - return "", err - } else { - logger.Info("projectName", "projectName", defaultCredentials.ProjectID) - return defaultCredentials.ProjectID, nil - } -} - func transformMetricDescriptorResponseToTable(data MetricDescriptorsResponse) *tsdb.Table { table := &tsdb.Table{ Columns: make([]tsdb.TableColumn, 1), diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 6e079a59fa3..d73704abb8e 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -221,8 +221,25 @@ export default class StackdriverDatasource { async getDefaultProject() { try { - await this.queryPromise; - return this.projectName; + if (!this.projectName) { + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [ + { + refId: 'defaultProject', + type: 'defaultProject', + datasourceId: this.id, + }, + ], + }, + }); + this.projectName = data.results.defaultProject.meta.defaultProject; + return this.projectName; + } else { + return this.projectName; + } } catch (error) { let message = 'Projects cannot be fetched: '; message += error.statusText ? error.statusText + ': ' : ''; From 4a51d054772743cede4843d0bedac99c7c08b863 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 13:38:21 +0200 Subject: [PATCH 090/118] stackdriver: add variable for gce authentication type --- pkg/tsdb/stackdriver/stackdriver.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 0395a7ca554..3e9bdfaee03 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -31,9 +31,10 @@ import ( ) var ( - slog log.Logger - legendKeyFormat *regexp.Regexp - metricNameFormat *regexp.Regexp + slog log.Logger + legendKeyFormat *regexp.Regexp + metricNameFormat *regexp.Regexp + gceAuthentication string ) // StackdriverExecutor executes queries for the Stackdriver datasource @@ -60,6 +61,7 @@ func init() { tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor) legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`) + gceAuthentication = "gce" } // Query takes in the frontend queries, parses them into the Stackdriver query format @@ -92,7 +94,7 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu } authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") - if authenticationType == "gce" { + if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) if err != nil { return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) @@ -585,11 +587,10 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) { authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") - if authenticationType == "gce" { + if authenticationType == gceAuthentication { defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") if err != nil { - return "raintank-production", nil - // return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) + return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) } else { logger.Info("projectName", "projectName", defaultCredentials.ProjectID) return defaultCredentials.ProjectID, nil From 8b4fff8b87cd1f587f17267735da52be7b41fe5b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 13:40:17 +0200 Subject: [PATCH 091/118] stackdriver: use gce variable --- pkg/tsdb/stackdriver/test_datasource_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index 7ed6cbb061d..5a495754bd8 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -21,7 +21,7 @@ func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQue } authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") - if authenticationType == "gce" { + if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) if err != nil { return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) From fa1d2257434b36bfa57348dc05b5f5ca122541f5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 13:48:41 +0200 Subject: [PATCH 092/118] stackdriver: fix failing test --- .../datasource/stackdriver/datasource.ts | 6 +-- .../stackdriver/specs/datasource.test.ts | 41 ++----------------- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index d73704abb8e..2d47493ffc9 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -198,13 +198,13 @@ export default class StackdriverDatasource { title: 'Success', }; } catch (error) { - console.log(error.data.error); let message = 'Stackdriver: '; message += error.statusText ? error.statusText + ': ' : ''; - - if (error.data && error.data.error && error.data.error) { + if (error.data && error.data.error) { try { + console.log(error.data.error); const res = JSON.parse(error.data.error); + console.log(res); message += res.error.code + '. ' + res.error.message; } catch (err) { message += error.data.error; diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index ab0c0653816..8707270477f 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -53,7 +53,9 @@ describe('StackdriverDataSource', () => { datasourceRequest: async () => Promise.reject({ statusText: 'Bad Request', - data: { error: { code: 400, message: 'Field interval.endTime had an invalid value' } }, + data: { + error: JSON.stringify({ error: { code: 400, message: 'Field interval.endTime had an invalid value' } }), + }, }), }; ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); @@ -67,43 +69,6 @@ describe('StackdriverDataSource', () => { }); }); - describe('when performing getProjects', () => { - describe('and call to resource manager api succeeds', () => { - let ds; - let result; - beforeEach(async () => { - const response = { - projects: [ - { - projectNumber: '853996325002', - projectId: 'test-project', - lifecycleState: 'ACTIVE', - name: 'Test Project', - createTime: '2015-06-02T14:16:08.520Z', - parent: { - type: 'organization', - id: '853996325002', - }, - }, - ], - }; - const backendSrv = { - async datasourceRequest() { - return Promise.resolve({ status: 200, data: response }); - }, - }; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); - result = await ds.getProjects(); - }); - - it('should return successfully', () => { - expect(result.length).toBe(1); - expect(result[0].id).toBe('test-project'); - expect(result[0].name).toBe('Test Project'); - }); - }); - }); - describe('When performing query', () => { const options = { range: { From 569fe2327e1588b0a92b916e391e16a65e5d9966 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 13:58:20 +0200 Subject: [PATCH 093/118] stackdriver: remove debug log --- public/app/plugins/datasource/stackdriver/datasource.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 2d47493ffc9..245ebc036cd 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -202,9 +202,7 @@ export default class StackdriverDatasource { message += error.statusText ? error.statusText + ': ' : ''; if (error.data && error.data.error) { try { - console.log(error.data.error); const res = JSON.parse(error.data.error); - console.log(res); message += res.error.code + '. ' + res.error.message; } catch (err) { message += error.data.error; From 3935f9d24c00ac13269c5f347d63327d8d391247 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 14:17:03 +0200 Subject: [PATCH 094/118] stackdriver: use constant instead of variable --- pkg/tsdb/stackdriver/stackdriver.go | 17 ++++++++++------- pkg/tsdb/stackdriver/test_datasource_query.go | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 3e9bdfaee03..e6d2af24018 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -31,10 +31,14 @@ import ( ) var ( - slog log.Logger - legendKeyFormat *regexp.Regexp - metricNameFormat *regexp.Regexp - gceAuthentication string + slog log.Logger + legendKeyFormat *regexp.Regexp + metricNameFormat *regexp.Regexp +) + +const ( + gceAuthentication string = "gce" + jwtAuthentication string = "jwt" ) // StackdriverExecutor executes queries for the Stackdriver datasource @@ -61,7 +65,6 @@ func init() { tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor) legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`) - gceAuthentication = "gce" } // Query takes in the frontend queries, parses them into the Stackdriver query format @@ -93,7 +96,7 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu Results: make(map[string]*tsdb.QueryResult), } - authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) if err != nil { @@ -586,7 +589,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. } func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) { - authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") if err != nil { diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index 5a495754bd8..c5968b6a081 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -20,7 +20,7 @@ func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQue Results: make(map[string]*tsdb.QueryResult), } - authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString("jwt") + authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) if err != nil { From 277cae2d852165339db8491e3e6b378e30d78987 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 14:45:14 +0200 Subject: [PATCH 095/118] stackdriver: fix broken condition after rebase --- .../app/plugins/datasource/stackdriver/partials/config.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 0cc08a3e9d8..31497d3e8e9 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -91,7 +91,7 @@

      Do not forget to + class="fa fa-save"> Do not forget to save your changes after uploading a file.

      - Verify GCE automatic authentication by clicking Save & Test

      + Verify GCE automatic authentication by clicking Save & Test

      \ No newline at end of file From 39995783006a3a652d1b565a4893451dc06f960d Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 14:46:28 +0200 Subject: [PATCH 096/118] stackdriver: fix broken braces --- 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 e6d2af24018..82c1e521c01 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -553,7 +553,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { return bucketBound } -func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, endpointName string)) (*http.Request, error) { +func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, endpointName string) (*http.Request, error) { u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "render") From b2c6c743ce8765e858c8b0fe42f84ab05bcb9534 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 17:33:38 +0200 Subject: [PATCH 097/118] stackdriver: add debug logging --- pkg/api/pluginproxy/ds_auth_provider.go | 1 + pkg/tsdb/stackdriver/stackdriver.go | 1 + pkg/tsdb/stackdriver/test_datasource_query.go | 2 ++ 3 files changed, 4 insertions(+) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 60360733923..c6afeb38879 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,6 +55,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } + logger.Info("ds-auth-provider-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt") if route.JwtTokenAuth != nil && authenticationType == "jwt" { logger.Info("authenticationType", "authenticationType", "jwt") diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 82c1e521c01..c42e5463f80 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -96,6 +96,7 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu Results: make(map[string]*tsdb.QueryResult), } + logger.Info("executeTimeSeriesQuery-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index c5968b6a081..be4bab38734 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/simplejson" "golang.org/x/net/context/ctxhttp" @@ -20,6 +21,7 @@ func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQue Results: make(map[string]*tsdb.QueryResult), } + logger.Info("executeTestDataSource-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) From afe0b4b8db58ebca0f8fbabf48b64c3deb6e4300 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Oct 2018 17:36:54 +0200 Subject: [PATCH 098/118] stackdriver: fix typo --- pkg/api/pluginproxy/ds_auth_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index c6afeb38879..4be75465a13 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,7 +55,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - logger.Info("ds-auth-provider-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) + logger.Info("ds-auth-provider-authenticationType", "debug", ds.JsonData.Get("authenticationType")) authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt") if route.JwtTokenAuth != nil && authenticationType == "jwt" { logger.Info("authenticationType", "authenticationType", "jwt") From a56c5841e907fddbabdbf372bedb94d840d15272 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 09:51:44 +0200 Subject: [PATCH 099/118] stackdriver: improve error handling in the datasource --- .../datasource/stackdriver/datasource.ts | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 245ebc036cd..57f8e00ac5c 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -198,25 +198,29 @@ export default class StackdriverDatasource { title: 'Success', }; } catch (error) { - let message = 'Stackdriver: '; - message += error.statusText ? error.statusText + ': ' : ''; - if (error.data && error.data.error) { - try { - const res = JSON.parse(error.data.error); - message += res.error.code + '. ' + res.error.message; - } catch (err) { - message += error.data.error; - } - } else { - message += 'Cannot connect to Stackdriver API'; - } return { status: 'error', - message: message, + message: this.formatStackdriverError(error), }; } } + formatStackdriverError(error) { + let message = 'Stackdriver: '; + message += error.statusText ? error.statusText + ': ' : ''; + if (error.data && error.data.error) { + try { + const res = JSON.parse(error.data.error); + message += res.error.code + '. ' + res.error.message; + } catch (err) { + message += error.data.error; + } + } else { + message += 'Cannot connect to Stackdriver API'; + } + return message; + } + async getDefaultProject() { try { if (!this.projectName) { @@ -239,21 +243,7 @@ export default class StackdriverDatasource { return this.projectName; } } catch (error) { - let message = 'Projects cannot be fetched: '; - message += error.statusText ? error.statusText + ': ' : ''; - if (error && error.data && error.data.error && error.data.error.message) { - if (error.data.error.code === 403) { - message += ` - A list of projects could not be fetched from the Google Cloud Resource Manager API. - You might need to enable it first: - https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`; - } else { - message += error.data.error.code + '. ' + error.data.error.message; - } - } else { - message += 'Cannot connect to Stackdriver API'; - } - appEvents.emit('ds-request-error', message); + appEvents.emit('ds-request-error', this.formatStackdriverError(error)); return ''; } } @@ -274,7 +264,8 @@ export default class StackdriverDatasource { return metrics; } catch (error) { - console.log(error); + appEvents.emit('ds-request-error', this.formatStackdriverError(error)); + return []; } } From b02a8127ff4683c6e033dccd740ae85a91464873 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 09:58:16 +0200 Subject: [PATCH 100/118] stackdriver: remove debug comments --- pkg/api/pluginproxy/ds_auth_provider.go | 3 --- pkg/tsdb/stackdriver/stackdriver.go | 3 --- pkg/tsdb/stackdriver/test_datasource_query.go | 2 -- 3 files changed, 8 deletions(-) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 4be75465a13..edf43085c7c 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -55,10 +55,8 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - logger.Info("ds-auth-provider-authenticationType", "debug", ds.JsonData.Get("authenticationType")) authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt") if route.JwtTokenAuth != nil && authenticationType == "jwt" { - logger.Info("authenticationType", "authenticationType", "jwt") if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) } else { @@ -67,7 +65,6 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } if authenticationType == "gce" { - logger.Info("authenticationType", "authenticationType", "gce") tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...) if err != nil { logger.Error("Failed to get default token from meta data server", "error", err) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index c42e5463f80..db8c2a0e501 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -19,7 +19,6 @@ import ( "golang.org/x/oauth2/google" "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" @@ -96,7 +95,6 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu Results: make(map[string]*tsdb.QueryResult), } - logger.Info("executeTimeSeriesQuery-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) @@ -596,7 +594,6 @@ func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, er if err != nil { return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) } else { - logger.Info("projectName", "projectName", defaultCredentials.ProjectID) return defaultCredentials.ProjectID, nil } } else { diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go index be4bab38734..c5968b6a081 100644 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ b/pkg/tsdb/stackdriver/test_datasource_query.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/simplejson" "golang.org/x/net/context/ctxhttp" @@ -21,7 +20,6 @@ func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQue Results: make(map[string]*tsdb.QueryResult), } - logger.Info("executeTestDataSource-authenticationType", "debug", e.dsInfo.JsonData.Get("authenticationType")) authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) if authenticationType == gceAuthentication { defaultProject, err := e.getDefaultProject(ctx) From 1a329e4e47355b55910cb600325e58815024aca9 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 13:44:15 +0200 Subject: [PATCH 101/118] stackdriver: remove test datasource query. use ensuredefaultproject instead --- .../stackdriver/ensure_default_project.go | 24 +++++ pkg/tsdb/stackdriver/stackdriver.go | 6 +- pkg/tsdb/stackdriver/test_datasource_query.go | 94 ------------------- 3 files changed, 26 insertions(+), 98 deletions(-) create mode 100644 pkg/tsdb/stackdriver/ensure_default_project.go delete mode 100644 pkg/tsdb/stackdriver/test_datasource_query.go diff --git a/pkg/tsdb/stackdriver/ensure_default_project.go b/pkg/tsdb/stackdriver/ensure_default_project.go new file mode 100644 index 00000000000..265fdda5151 --- /dev/null +++ b/pkg/tsdb/stackdriver/ensure_default_project.go @@ -0,0 +1,24 @@ +package stackdriver + +import ( + "context" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" +) + +func (e *StackdriverExecutor) ensureDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} + result := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + defaultProject, err := e.getDefaultProject(ctx) + if err != nil { + return nil, err + } + + e.dsInfo.JsonData.Set("defaultProject", defaultProject) + queryResult.Meta.Set("defaultProject", defaultProject) + result.Results[tsdbQuery.Queries[0].RefId] = queryResult + return result, nil +} diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index db8c2a0e501..ad5dded38b2 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -77,10 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour switch queryType { case "annotationQuery": result, err = e.executeAnnotationQuery(ctx, tsdbQuery) - case "testDatasource": - result, err = e.executeTestDataSource(ctx, tsdbQuery) - case "defaultProject": - result, err = e.getGceDefaultProject(ctx, tsdbQuery) + case "ensureDefaultProjectQuery": + result, err = e.ensureDefaultProject(ctx, tsdbQuery) case "timeSeriesQuery": fallthrough default: diff --git a/pkg/tsdb/stackdriver/test_datasource_query.go b/pkg/tsdb/stackdriver/test_datasource_query.go deleted file mode 100644 index c5968b6a081..00000000000 --- a/pkg/tsdb/stackdriver/test_datasource_query.go +++ /dev/null @@ -1,94 +0,0 @@ -package stackdriver - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - - "github.com/grafana/grafana/pkg/components/simplejson" - "golang.org/x/net/context/ctxhttp" - - "github.com/grafana/grafana/pkg/tsdb" -) - -func (e *StackdriverExecutor) executeTestDataSource(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} - result := &tsdb.Response{ - Results: make(map[string]*tsdb.QueryResult), - } - - authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication) - if authenticationType == gceAuthentication { - defaultProject, err := e.getDefaultProject(ctx) - if err != nil { - return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) - } - - e.dsInfo.JsonData.Set("defaultProject", defaultProject) - } - - req, err := e.createRequest(ctx, e.dsInfo, "metricDescriptors") - if err != nil { - slog.Error("Failed to create request", "error", err) - return nil, fmt.Errorf("Failed to create request. error: %v", err) - } - res, err := ctxhttp.Do(ctx, e.httpClient, req) - if err != nil { - return nil, err - } - - data, err := e.unmarshalMetricDescriptors(res) - if err != nil { - return nil, err - } - - parts := strings.Split(req.URL.Path, "/") - defaultProject := parts[3] - - table := transformMetricDescriptorResponseToTable(data) - queryResult.Tables = append(queryResult.Tables, table) - result.Results[tsdbQuery.Queries[0].RefId] = queryResult - result.Results[tsdbQuery.Queries[0].RefId].Meta.Set("defaultProject", defaultProject) - - return result, nil -} - -func transformMetricDescriptorResponseToTable(data MetricDescriptorsResponse) *tsdb.Table { - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, 1), - Rows: make([]tsdb.RowValues, 0), - } - table.Columns[0].Text = "metricDescriptor" - - for _, r := range data.MetricDescriptors { - values := make([]interface{}, 1) - values[0] = r - table.Rows = append(table.Rows, values) - } - return table -} - -func (e *StackdriverExecutor) unmarshalMetricDescriptors(res *http.Response) (MetricDescriptorsResponse, error) { - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return MetricDescriptorsResponse{}, err - } - - if res.StatusCode/100 != 2 { - slog.Error("Request failed", "status", res.Status, "body", string(body)) - return MetricDescriptorsResponse{}, fmt.Errorf(`%s`, string(body)) - } - - var data MetricDescriptorsResponse - err = json.Unmarshal(body, &data) - if err != nil { - slog.Error("Failed to unmarshal MetricDescriptorResponse", "error", err, "status", res.Status, "body", string(body)) - return MetricDescriptorsResponse{}, err - } - - return data, nil -} From 5b04a8b6c299ca89adfbb7ef97dabac5bfeb10ae Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 13:45:38 +0200 Subject: [PATCH 102/118] stackdriver: remove not used query file --- pkg/tsdb/stackdriver/default_project_query.go | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 pkg/tsdb/stackdriver/default_project_query.go diff --git a/pkg/tsdb/stackdriver/default_project_query.go b/pkg/tsdb/stackdriver/default_project_query.go deleted file mode 100644 index 16287fd09ff..00000000000 --- a/pkg/tsdb/stackdriver/default_project_query.go +++ /dev/null @@ -1,22 +0,0 @@ -package stackdriver - -import ( - "context" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/tsdb" -) - -func (e *StackdriverExecutor) getGceDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId} - result := &tsdb.Response{ - Results: make(map[string]*tsdb.QueryResult), - } - defaultProject, err := e.getDefaultProject(ctx) - if err != nil { - return nil, err - } - queryResult.Meta.Set("defaultProject", defaultProject) - result.Results[tsdbQuery.Queries[0].RefId] = queryResult - return result, nil -} From ae8a76519038d59ce47e41e979ab724613e0f9d5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 13:46:51 +0200 Subject: [PATCH 103/118] stackdriver: improve error handling --- .../datasource/stackdriver/datasource.ts | 61 +++++++++++-------- .../stackdriver/query_filter_ctrl.ts | 13 ++-- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 57f8e00ac5c..05af784bb8c 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -1,11 +1,13 @@ import { stackdriverUnitMappings } from './constants'; import appEvents from 'app/core/app_events'; +import _ from 'lodash'; export default class StackdriverDatasource { id: number; url: string; baseUrl: string; projectName: string; + authenticationType: string; queryPromise: Promise; /** @ngInject */ @@ -15,6 +17,7 @@ export default class StackdriverDatasource { this.doRequest = this.doRequest; this.id = instanceSettings.id; this.projectName = instanceSettings.jsonData.defaultProject || ''; + this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt'; } async getTimeSeries(options) { @@ -178,29 +181,36 @@ export default class StackdriverDatasource { } async testDatasource() { + let status, message; + const defaultErrorMessage = 'Cannot connect to Stackdriver API'; try { - await this.backendSrv.datasourceRequest({ - url: '/api/tsdb/query', - method: 'POST', - data: { - queries: [ - { - refId: 'testDatasource', - datasourceId: this.id, - type: 'testDatasource', - }, - ], - }, - }); - return { - status: 'success', - message: 'Successfully queried the Stackdriver API.', - title: 'Success', - }; + const projectName = await this.getDefaultProject(); + const path = `v3/projects/${projectName}/metricDescriptors`; + const response = await this.doRequest(`${this.baseUrl}${path}`); + if (response.status === 200) { + status = 'success'; + message = 'Successfully queried the Stackdriver API.'; + } else { + status = 'error'; + message = response.statusText ? response.statusText : defaultErrorMessage; + } } catch (error) { + status = 'error'; + if (_.isString(error)) { + message = error; + } else { + message = 'Stackdriver: '; + message += error.statusText ? error.statusText + ': ' : ''; + if (error.data && error.data.error && error.data.error.code) { + message += error.data.error.code + '. ' + error.data.error.message; + } else { + message = defaultErrorMessage; + } + } + } finally { return { - status: 'error', - message: this.formatStackdriverError(error), + status, + message, }; } } @@ -223,28 +233,27 @@ export default class StackdriverDatasource { async getDefaultProject() { try { - if (!this.projectName) { + if (this.authenticationType === 'gce' || !this.projectName) { const { data } = await this.backendSrv.datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: { queries: [ { - refId: 'defaultProject', - type: 'defaultProject', + refId: 'ensureDefaultProjectQuery', + type: 'ensureDefaultProjectQuery', datasourceId: this.id, }, ], }, }); - this.projectName = data.results.defaultProject.meta.defaultProject; + this.projectName = data.results.ensureDefaultProjectQuery.meta.defaultProject; return this.projectName; } else { return this.projectName; } } catch (error) { - appEvents.emit('ds-request-error', this.formatStackdriverError(error)); - return ''; + throw this.formatStackdriverError(error); } } diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 75e4817f395..df5fb0f2965 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -79,11 +79,16 @@ export class StackdriverFilterCtrl { } async getCurrentProject() { - return new Promise(async resolve => { - if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') { - this.target.defaultProject = await this.datasource.getDefaultProject(); + return new Promise(async (resolve, reject) => { + try { + if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') { + this.target.defaultProject = await this.datasource.getDefaultProject(); + } + resolve(this.target.defaultProject); + } catch (error) { + appEvents.emit('ds-request-error', error); + reject(); } - resolve(this.target.defaultProject); }); } From 5deba92d052537b5c87ee1640a38580919afeef3 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 14:44:51 +0200 Subject: [PATCH 104/118] stackdriver: fix failing tests --- public/app/plugins/datasource/stackdriver/datasource.ts | 6 ++---- .../plugins/datasource/stackdriver/specs/datasource.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 05af784bb8c..7497edb95a8 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -200,11 +200,9 @@ export default class StackdriverDatasource { message = error; } else { message = 'Stackdriver: '; - message += error.statusText ? error.statusText + ': ' : ''; + message += error.statusText ? error.statusText : defaultErrorMessage; if (error.data && error.data.error && error.data.error.code) { - message += error.data.error.code + '. ' + error.data.error.message; - } else { - message = defaultErrorMessage; + message += ': ' + error.data.error.code + '. ' + error.data.error.message; } } } finally { diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 8707270477f..d0d8462e506 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -6,7 +6,7 @@ import { TemplateSrvStub } from 'test/specs/helpers'; describe('StackdriverDataSource', () => { const instanceSettings = { jsonData: { - projectName: 'testproject', + defaultProject: 'testproject', }, }; const templateSrv = new TemplateSrvStub(); @@ -54,7 +54,7 @@ describe('StackdriverDataSource', () => { Promise.reject({ statusText: 'Bad Request', data: { - error: JSON.stringify({ error: { code: 400, message: 'Field interval.endTime had an invalid value' } }), + error: { code: 400, message: 'Field interval.endTime had an invalid value' }, }, }), }; From dd745e0cf0a2a7c1ead84e26c538fd8591183641 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Oct 2018 15:42:44 +0200 Subject: [PATCH 105/118] stackdriver: prevent backend query from being made when there are not yet any defined targets --- .../datasource/stackdriver/datasource.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7497edb95a8..cda952c23b9 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -50,16 +50,20 @@ export default class StackdriverDatasource { }; }); - const { data } = await this.backendSrv.datasourceRequest({ - url: '/api/tsdb/query', - method: 'POST', - data: { - from: options.range.from.valueOf().toString(), - to: options.range.to.valueOf().toString(), - queries, - }, - }); - return data; + if (queries.length > 0) { + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries, + }, + }); + return data; + } else { + return { results: [] }; + } } async getLabels(metricType, refId) { From 5b9cd830d284746412075a277c23ae8c5b5f7756 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 15 Oct 2018 16:31:33 +0200 Subject: [PATCH 106/118] stackdriver: improve config page info box documentation --- .../stackdriver/partials/config.html | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 31497d3e8e9..da8c9896348 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -1,26 +1,34 @@
      -
      GCP Service Account
      +

      Stackdriver Authentication

      +

      There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by + automatically retrieving credentials from Google metadata server. The latter option is only available + when running Grafana on GCE virtual machine.

      + +
      Uploading a JWT file

      - To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for + First you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.

      - The Monitoring Viewer role provides all the permissions that Grafana needs. + The Monitoring Viewer role provides all the permissions that Grafana needs. The following API + needs to be enabled on GCP for the datasource to work: Monitoring + API

      + +
      GCE Default Service Account

      - The following APIs need to be enabled on GCP for the datasource to work: -

      + If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to + automatically retrieve default project id and authentication token from the metadata server. In order for this to + work, you need to make sure that you have a service account that is setup as default account for the virtual + machine and that the service account has been given read access to the Stackdriver Monitoring API.

      +

      Detailed instructions on how to create a Service Account can be found in - the documentation.

      + the documentation. +

      From e13e2f8a45d6c60802b9477cf98640cfe5d7c245 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 15 Oct 2018 16:35:43 +0200 Subject: [PATCH 107/118] stackdriver: change name of authentication type --- public/app/plugins/datasource/stackdriver/config_ctrl.ts | 2 +- .../app/plugins/datasource/stackdriver/partials/config.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/config_ctrl.ts b/public/app/plugins/datasource/stackdriver/config_ctrl.ts index 9649a8772e9..e74659536c8 100644 --- a/public/app/plugins/datasource/stackdriver/config_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/config_ctrl.ts @@ -20,7 +20,7 @@ export class StackdriverConfigCtrl { this.current.secureJsonFields = this.current.secureJsonFields || {}; this.authenticationTypes = [ { key: this.defaultAuthenticationType, value: 'Google JWT File' }, - { key: 'gce', value: 'Use GCE default Authentication' }, + { key: 'gce', value: 'GCE Default Service Account' }, ]; } diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index da8c9896348..80158538a6e 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -34,7 +34,7 @@
      -

      Service Account Authentication

      +

      Authentication

      Upload your Service Account key file or paste in the contents of the file. The file contents will be encrypted and saved in the Grafana database.
      @@ -102,4 +102,4 @@ class="fa fa-save">
      Do not forget to save your changes after uploading a file.

      - Verify GCE automatic authentication by clicking Save & Test

      \ No newline at end of file + Verify GCE default service account by clicking Save & Test

      \ No newline at end of file From 0109051b2ae2c4f48c2762d91df6defe2eef3856 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 15 Oct 2018 17:26:19 +0200 Subject: [PATCH 108/118] stackdriver: add documentation for gce default account authentication --- docs/sources/features/datasources/stackdriver.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 103a58587b1..166ee9bcf68 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -35,7 +35,9 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat ## Authentication -### Service Account Credentials - Private Key File +There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine. + +### Using a Google JWT File To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project. @@ -74,6 +76,16 @@ Click on the links above and click the `Enable` button: {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}} +### Using GCE Default Service Account + +If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that needs to be met. + +1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount). +2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using). +3. Allow access to the `Stackdriver Monitoring API` scope. See instructions [here](changeserviceaccountandscopes). + +Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances). + ## Metric Query Editor {{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}} From a1fee2fd8adcc902ae28a0cdd73a897a075d9d74 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 16 Oct 2018 09:38:43 +0200 Subject: [PATCH 109/118] stackdriver: add provisioning example for gce authentication --- docs/sources/features/datasources/stackdriver.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 166ee9bcf68..2253c3c2163 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -206,7 +206,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources) -Here is a provisioning example for this datasource. +Here is a provisioning example using JWT authentication type. ```yaml apiVersion: 1 @@ -218,6 +218,7 @@ datasources: jsonData: tokenUri: https://oauth2.googleapis.com/token clientEmail: stackdriver@myproject.iam.gserviceaccount.com + authenticationType: jwt defaultProject: my-project-name secureJsonData: privateKey: | @@ -227,3 +228,16 @@ datasources: yA+23427282348234= -----END PRIVATE KEY----- ``` + +Here is a provisioning example using GCE Default Service Account authentication. + +```yaml +apiVersion: 1 + +datasources: + - name: Stackdriver + type: stackdriver + access: proxy + jsonData: + authenticationType: gce +``` From 89958f293396f5de0c27cbab46da60f5d20439eb Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 16 Oct 2018 09:47:21 +0200 Subject: [PATCH 110/118] stackdriver: remove not needed scope --- public/app/plugins/datasource/stackdriver/plugin.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index 06a6880002b..2ec44ae4ca5 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -28,10 +28,7 @@ "method": "GET", "url": "https://content-monitoring.googleapis.com", "jwtTokenAuth": { - "scopes": [ - "https://www.googleapis.com/auth/monitoring.read", - "https://www.googleapis.com/auth/cloudplatformprojects.readonly" - ], + "scopes": ["https://www.googleapis.com/auth/monitoring.read"], "params": { "token_uri": "{{.JsonData.tokenUri}}", "client_email": "{{.JsonData.clientEmail}}", From 1756e82d8bd334f41b1c305d781ac7ab638c589f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 16 Oct 2018 09:52:14 +0200 Subject: [PATCH 111/118] stackdriver: remove not used struct --- pkg/tsdb/stackdriver/types.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index de6daa5dc06..3821ce7ceda 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -73,24 +73,3 @@ type StackdriverResponse struct { } `json:"points"` } `json:"timeSeries"` } - -type MetricDescriptorsResponse struct { - MetricDescriptors []struct { - Name string `json:"name"` - Labels []struct { - Key string `json:"key"` - Description string `json:"description"` - } `json:"labels,omitempty"` - MetricKind string `json:"metricKind"` - ValueType string `json:"valueType"` - Unit string `json:"unit,omitempty"` - Description string `json:"description"` - DisplayName string `json:"displayName"` - Type string `json:"type"` - Metadata struct { - LaunchStage string `json:"launchStage"` - SamplePeriod string `json:"samplePeriod"` - IngestDelay string `json:"ingestDelay"` - } `json:"metadata"` - } `json:"metricDescriptors"` -} From 6abbf5b3b868937e8661baea3a0105bf3a441f81 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 16 Oct 2018 09:56:49 +0200 Subject: [PATCH 112/118] stackdriver: refactoring request builder --- pkg/tsdb/stackdriver/stackdriver.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index ad5dded38b2..8b903ba0113 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -283,7 +283,7 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) { 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, "timeSeries") + req, err := e.createRequest(ctx, e.dsInfo) if err != nil { queryResult.Error = err return queryResult, StackdriverResponse{}, nil @@ -550,7 +550,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { return bucketBound } -func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource, endpointName string) (*http.Request, error) { +func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "render") @@ -578,7 +578,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. } projectName := dsInfo.JsonData.Get("defaultProject").MustString() - proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/"+endpointName) + proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries") pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) @@ -591,10 +591,8 @@ func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, er defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read") if err != nil { return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err) - } else { - return defaultCredentials.ProjectID, nil } - } else { - return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil + return defaultCredentials.ProjectID, nil } + return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil } From 6e0728ad8833ec683a59a3bc2ec5350827934956 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 16 Oct 2018 18:53:40 +0200 Subject: [PATCH 113/118] stackdriver: some tweaks to the text on the config page and in docs --- docs/sources/features/datasources/stackdriver.md | 6 +++--- .../datasource/stackdriver/partials/config.html | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 2253c3c2163..3ae2ed3df40 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -37,7 +37,7 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine. -### Using a Google JWT File +### Using a Google Service Account Key File To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project. @@ -78,7 +78,7 @@ Click on the links above and click the `Enable` button: ### Using GCE Default Service Account -If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that needs to be met. +If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that need to be met. 1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount). 2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using). @@ -206,7 +206,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources) -Here is a provisioning example using JWT authentication type. +Here is a provisioning example using the JWT (Service Account key file) authentication type. ```yaml apiVersion: 1 diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index 80158538a6e..9be7eef0e68 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -1,11 +1,11 @@

      Stackdriver Authentication

      -

      There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by - automatically retrieving credentials from Google metadata server. The latter option is only available - when running Grafana on GCE virtual machine.

      +

      There are two ways to authenticate the Stackdriver plugin - either by uploading a Service Account key file, or by + automatically retrieving credentials from the Google metadata server. The latter option is only available + when running Grafana on a GCE virtual machine.

      -
      Uploading a JWT file
      +
      Uploading a Service Account Key File

      First you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to @@ -20,8 +20,8 @@

      GCE Default Service Account

      If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to - automatically retrieve default project id and authentication token from the metadata server. In order for this to - work, you need to make sure that you have a service account that is setup as default account for the virtual + automatically retrieve the default project id and authentication token from the metadata server. In order for this to + work, you need to make sure that you have a service account that is setup as the default account for the virtual machine and that the service account has been given read access to the Stackdriver Monitoring API.

      @@ -102,4 +102,4 @@ class="fa fa-save"> Do not forget to save your changes after uploading a file.

      - Verify GCE default service account by clicking Save & Test

      \ No newline at end of file + Verify GCE default service account by clicking Save & Test

      From ead6a051df5c0f8e97c141d0a1c15420a1035dd1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 22 Oct 2018 18:31:31 +0200 Subject: [PATCH 114/118] docs: update debian installation instructions --- docs/sources/installation/debian.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 2ae2e9dc40d..7ed44572533 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -28,7 +28,7 @@ installation. ```bash wget sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.4_amd64.deb +sudo dpkg -i grafana__amd64.deb ``` Example: From 41018fc7882d87b05de1610fa4f0f462069aef5a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 22 Oct 2018 18:52:53 +0200 Subject: [PATCH 115/118] changelog: adds note for #13669 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5457147f5..f10c712f34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset) * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro) * **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) +* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) ### Minor From e6abc0691e3416b10f5fb08d219c496742f42aad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 22 Oct 2018 19:22:59 +0200 Subject: [PATCH 116/118] fix injecting dependencies to graph legend directive --- public/app/plugins/panel/graph/legend.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 7b01c46c4d3..db4dfdefa1c 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -3,7 +3,8 @@ import $ from 'jquery'; import baron from 'baron'; import coreModule from 'app/core/core_module'; -coreModule.directive('graphLegend', (popoverSrv, $timeout) => { +/** @ngInject */ +function graphLegendDirective(popoverSrv, $timeout) { return { link: (scope, elem) => { let firstRender = true; @@ -300,4 +301,6 @@ coreModule.directive('graphLegend', (popoverSrv, $timeout) => { } }, }; -}); +} + +coreModule.directive('graphLegend', graphLegendDirective); From 3aa0a0ca983b500ace3b55b0d93f7d6be5462d5c Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 23 Oct 2018 08:33:52 +0200 Subject: [PATCH 117/118] build: fixes filename issues. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87cd502ef18..7e4979de572 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -242,7 +242,7 @@ jobs: command: '/tmp/bootstrap.sh' - run: name: checkout enterprise - command: './scripts/build/prepare_enterprise.sh' + command: './scripts/build/prepare-enterprise.sh' - run: name: test enterprise command: 'go test ./pkg/extensions/...' @@ -274,7 +274,7 @@ jobs: command: '/tmp/bootstrap.sh' - run: name: checkout enterprise - command: './scripts/build/prepare_enterprise.sh' + command: './scripts/build/prepare-enterprise.sh' - restore_cache: key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }} - run: From ab4cc6a9bfbe431bedc4ba9826be0b63b1c76adf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 23 Oct 2018 08:53:36 +0200 Subject: [PATCH 118/118] build: indentation mistake. --- .circleci/config.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e4979de572..c293ea26a9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -323,18 +323,18 @@ jobs: name: deploy to s3 command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master' -deploy-enterprise-release: - docker: - - image: circleci/python:2.7-stretch - steps: - - attach_workspace: - at: . - - run: - name: install awscli - command: 'sudo pip install awscli' - - run: - name: deploy to s3 - command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release' + deploy-enterprise-release: + docker: + - image: circleci/python:2.7-stretch + steps: + - attach_workspace: + at: . + - run: + name: install awscli + command: 'sudo pip install awscli' + - run: + name: deploy to s3 + command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release' deploy-master: docker: