From 6a4777eafcaf2acc5092eeea03e981ec09565eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 31 Jan 2019 09:44:12 +0100 Subject: [PATCH 1/2] wip: New react container route for solo panels that supports both angular and react panels --- .../dashboard/containers/SoloPanelPage.tsx | 101 ++++++++++++++++++ .../dashboard/state/DashboardModel.ts | 2 +- public/app/routes/ReactContainer.tsx | 8 +- public/app/routes/routes.ts | 10 +- public/sass/pages/_dashboard.scss | 7 ++ 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 public/app/features/dashboard/containers/SoloPanelPage.tsx diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx new file mode 100644 index 00000000000..9c6b13d6676 --- /dev/null +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -0,0 +1,101 @@ +// Libraries +import React, { Component } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +// Utils & Services +import appEvents from 'app/core/app_events'; + +// Components +import { DashboardPanel } from '../dashgrid/DashboardPanel'; + +// Types +import { StoreState } from 'app/types'; +import { PanelModel, DashboardModel } from 'app/features/dashboard/state'; + +interface Props { + panelId: string; + uid?: string; + slug?: string; + type?: string; + $scope: any; + $injector: any; +} + +interface State { + panel: PanelModel | null; + dashboard: DashboardModel | null; + notFound: boolean; +} + +export class SoloPanelPage extends Component { + + state: State = { + panel: null, + dashboard: null, + notFound: false, + }; + + componentDidMount() { + const { $injector, $scope, uid } = this.props; + + const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv'); + + // subscribe to event to know when dashboard controller is done with inititalization + appEvents.on('dashboard-initialized', this.onDashoardInitialized); + + dashboardLoaderSrv.loadDashboard('', '', uid).then(result => { + result.meta.soloMode = true; + $scope.initDashboard(result, $scope); + }); + } + + onDashoardInitialized = () => { + const { $scope, panelId } = this.props; + + const dashboard: DashboardModel = $scope.dashboard; + const panel = dashboard.getPanelById(parseInt(panelId, 10)); + + if (!panel) { + this.setState({ notFound: true }); + return; + } + + this.setState({ dashboard, panel }); + }; + + render() { + const { panelId } = this.props; + const { notFound, panel, dashboard } = this.state; + + if (notFound) { + return ( +
+ Panel with id { panelId } not found +
+ ); + } + + if (!panel) { + return
Loading & initializing dashboard
; + } + + return ( +
+ +
+ ); + } +} + +const mapStateToProps = (state: StoreState) => ({ + uid: state.location.routeParams.uid, + slug: state.location.routeParams.slug, + type: state.location.routeParams.type, + panelId: state.location.query.panelId +}); + +const mapDispatchToProps = { +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage)); diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 06dc9450425..929b984a93f 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -271,7 +271,7 @@ export class DashboardModel { } } - getPanelById(id) { + getPanelById(id): PanelModel { for (const panel of this.panels) { if (panel.id === id) { return panel; diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 19cdff03b69..2cad3d828bf 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -18,6 +18,8 @@ function WrapInProvider(store, Component, props) { export function reactContainer( $route, $location, + $injector, + $rootScope, contextSrv: ContextSrv ) { return { @@ -38,7 +40,11 @@ export function reactContainer( component = component.default; } - const props = { }; + const props = { + $injector: $injector, + $rootScope: $rootScope, + $scope: scope, + }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index e7381740435..3f4b2fb074d 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -19,6 +19,7 @@ import UsersListPage from 'app/features/users/UsersListPage'; import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards'; import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage'; import OrgDetailsPage from '../features/org/OrgDetailsPage'; +import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage'; import config from 'app/core/config'; /** @ngInject */ @@ -51,10 +52,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { pageClass: 'page-dashboard', }) .when('/d-solo/:uid/:slug', { - templateUrl: 'public/app/features/panel/partials/soloPanel.html', - controller: 'SoloPanelCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', + template: '', + pageClass: 'dashboard-solo', + resolve: { + component: () => SoloPanelPage, + }, }) .when('/dashboard-solo/:type/:slug', { templateUrl: 'public/app/features/panel/partials/soloPanel.html', diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index a0ff9fd877c..9ca4e092f02 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -17,6 +17,13 @@ div.flot-text { height: 100%; } +.dashboard-solo { + .footer, + .sidemenu { + display: none; + } +} + .panel-solo { position: fixed; bottom: 0; From aeaac7480bc3b7f3ae13d80f3830e85a7eb29df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 Feb 2019 08:12:58 +0100 Subject: [PATCH 2/2] New solo panel route working in all scenarios I can test --- public/app/core/profiler.ts | 103 +----------------- .../dashboard/containers/SoloPanelPage.tsx | 38 +++++-- .../features/dashboard/dashgrid/DataPanel.tsx | 3 - .../dashboard/dashgrid/PanelChrome.tsx | 9 +- public/app/features/panel/all.ts | 1 - .../app/features/panel/metrics_panel_ctrl.ts | 11 -- public/app/features/panel/panel_ctrl.ts | 25 +---- public/app/features/panel/panel_directive.ts | 14 ++- .../features/panel/partials/soloPanel.html | 4 - public/app/features/panel/solo_panel_ctrl.ts | 58 ---------- public/app/plugins/panel/table/module.ts | 1 - public/app/routes/routes.ts | 9 +- 12 files changed, 58 insertions(+), 218 deletions(-) delete mode 100644 public/app/features/panel/partials/soloPanel.html delete mode 100644 public/app/features/panel/solo_panel_ctrl.ts diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts index 0e738dd3da1..19c67039c41 100644 --- a/public/app/core/profiler.ts +++ b/public/app/core/profiler.ts @@ -1,106 +1,20 @@ -import $ from 'jquery'; -import angular from 'angular'; export class Profiler { panelsRendered: number; enabled: boolean; - panelsInitCount: any; - timings: any; - digestCounter: any; $rootScope: any; - scopeCount: any; window: any; init(config, $rootScope) { - this.enabled = config.buildInfo.env === 'development'; - this.timings = {}; - this.timings.appStart = { loadStart: new Date().getTime() }; this.$rootScope = $rootScope; this.window = window; if (!this.enabled) { return; } - - $rootScope.$watch( - () => { - this.digestCounter++; - return false; - }, - () => {} - ); - - $rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope); - $rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope); - $rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope); - $rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope); } - refresh() { - this.timings.query = 0; - this.timings.render = 0; - - setTimeout(() => { - console.log('panel count: ' + this.panelsInitCount); - console.log('total query: ' + this.timings.query); - console.log('total render: ' + this.timings.render); - console.log('avg render: ' + this.timings.render / this.panelsInitCount); - }, 5000); - } - - dashboardFetched() { - this.timings.dashboardLoadStart = new Date().getTime(); - this.panelsInitCount = 0; - this.digestCounter = 0; - this.panelsInitCount = 0; - this.panelsRendered = 0; - this.timings.query = 0; - this.timings.render = 0; - } - - dashboardInitialized() { - setTimeout(() => { - console.log('Dashboard::Performance Total Digests: ' + this.digestCounter); - console.log('Dashboard::Performance Total Watchers: ' + this.getTotalWatcherCount()); - console.log('Dashboard::Performance Total ScopeCount: ' + this.scopeCount); - - const timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart; - console.log('Dashboard::Performance All panels initialized in ' + timeTaken + ' ms'); - - // measure digest performance - const rootDigestStart = window.performance.now(); - for (let i = 0; i < 30; i++) { - this.$rootScope.$apply(); - } - - console.log('Dashboard::Performance Root Digest ' + (window.performance.now() - rootDigestStart) / 30); - }, 3000); - } - - getTotalWatcherCount() { - let count = 0; - let scopes = 0; - const root = $(document.getElementsByTagName('body')); - - const f = element => { - if (element.data().hasOwnProperty('$scope')) { - scopes++; - angular.forEach(element.data().$scope.$$watchers, () => { - count++; - }); - } - - angular.forEach(element.children(), childElement => { - f($(childElement)); - }); - }; - - f(root); - this.scopeCount = scopes; - return count; - } - - renderingCompleted(panelId, panelTimings) { + renderingCompleted(panelId) { // add render counter to root scope // used by phantomjs render.js to know when panel has rendered this.panelsRendered = (this.panelsRendered || 0) + 1; @@ -108,21 +22,6 @@ export class Profiler { // this window variable is used by backend rendering tools to know // all panels have completed rendering this.window.panelsRendered = this.panelsRendered; - - if (this.enabled) { - panelTimings.renderEnd = new Date().getTime(); - this.timings.query += panelTimings.queryEnd - panelTimings.queryStart; - this.timings.render += panelTimings.renderEnd - panelTimings.renderStart; - } - } - - panelInitialized() { - if (!this.enabled) { - return; - } - - this.panelsInitCount++; - this.timings.lastPanelInitializedAt = new Date().getTime(); } } diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 9c6b13d6676..097c8015929 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -5,21 +5,27 @@ import { connect } from 'react-redux'; // Utils & Services import appEvents from 'app/core/app_events'; +import locationUtil from 'app/core/utils/location_util'; +import { getBackendSrv } from 'app/core/services/backend_srv'; // Components import { DashboardPanel } from '../dashgrid/DashboardPanel'; +// Redux +import { updateLocation } from 'app/core/actions'; + // Types import { StoreState } from 'app/types'; import { PanelModel, DashboardModel } from 'app/features/dashboard/state'; interface Props { panelId: string; - uid?: string; - slug?: string; - type?: string; + urlUid?: string; + urlSlug?: string; + urlType?: string; $scope: any; $injector: any; + updateLocation: typeof updateLocation; } interface State { @@ -37,19 +43,34 @@ export class SoloPanelPage extends Component { }; componentDidMount() { - const { $injector, $scope, uid } = this.props; + const { $injector, $scope, urlUid, urlType, urlSlug } = this.props; + + // handle old urls with no uid + if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { + this.redirectToNewUrl(); + return; + } const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv'); // subscribe to event to know when dashboard controller is done with inititalization appEvents.on('dashboard-initialized', this.onDashoardInitialized); - dashboardLoaderSrv.loadDashboard('', '', uid).then(result => { + dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => { result.meta.soloMode = true; $scope.initDashboard(result, $scope); }); } + redirectToNewUrl() { + getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => { + if (res) { + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + this.props.updateLocation(url); + } + }); + } + onDashoardInitialized = () => { const { $scope, panelId } = this.props; @@ -89,13 +110,14 @@ export class SoloPanelPage extends Component { } const mapStateToProps = (state: StoreState) => ({ - uid: state.location.routeParams.uid, - slug: state.location.routeParams.slug, - type: state.location.routeParams.type, + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, panelId: state.location.query.panelId }); const mapDispatchToProps = { + updateLocation }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage)); diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 353c474bcd9..2183548000b 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -135,11 +135,8 @@ export class DataPanel extends Component { cacheTimeout: null, }; - console.log('Issuing DataPanel query', queryOptions); const resp = await ds.query(queryOptions); - console.log('Issuing DataPanel query Resp', resp); - if (this.isUnmounted) { return; } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 68b53714504..99e206c6f51 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -12,11 +12,12 @@ import { DataPanel } from './DataPanel'; // Utils import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; +import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { TimeRange } from '@grafana/ui'; +import { TimeRange, LoadingState } from '@grafana/ui'; import variables from 'sass/_variables.scss'; import templateSrv from 'app/features/templating/template_srv'; @@ -98,6 +99,12 @@ export class PanelChrome extends PureComponent { const { timeRange, renderCounter } = this.state; const PanelComponent = plugin.exports.Panel; + // This is only done to increase a counter that is used by backend + // image rendering (phantomjs/headless chrome) to know when to capture image + if (loading === LoadingState.Done) { + profiler.renderingCompleted(panel.id); + } + return (
{ }); ctrl.events.on('panel-size-changed', () => { - ctrl.calculatePanelHeight(); + ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); $timeout(() => { resizeScrollableContent(); ctrl.render(); @@ -112,19 +112,21 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { // first wait one pass for dashboard fullscreen view mode to take effect (classses being applied) setTimeout(() => { // then recalc style - ctrl.calculatePanelHeight(); + ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); // then wait another cycle (this might not be needed) $timeout(() => { ctrl.render(); resizeScrollableContent(); }); - }); + }, 10); }); - // set initial height - ctrl.calculatePanelHeight(); - ctrl.events.on('render', () => { + // set initial height + if (!ctrl.height) { + ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); + } + if (transparentLastState !== ctrl.panel.transparent) { panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true); transparentLastState = ctrl.panel.transparent; diff --git a/public/app/features/panel/partials/soloPanel.html b/public/app/features/panel/partials/soloPanel.html deleted file mode 100644 index 644bbe74ffb..00000000000 --- a/public/app/features/panel/partials/soloPanel.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts deleted file mode 100644 index a8bf5371913..00000000000 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ /dev/null @@ -1,58 +0,0 @@ -import angular from 'angular'; -import locationUtil from 'app/core/utils/location_util'; -import appEvents from 'app/core/app_events'; - -export class SoloPanelCtrl { - /** @ngInject */ - constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) { - let panelId; - - $scope.init = () => { - contextSrv.sidemenu = false; - appEvents.emit('toggle-sidemenu-hidden'); - - const params = $location.search(); - panelId = parseInt(params.panelId, 10); - - appEvents.on('dashboard-initialized', $scope.initPanelScope); - - // if no uid, redirect to new route based on slug - if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { - backendSrv.getDashboardBySlug($routeParams.slug).then(res => { - if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - $location.path(url).replace(); - } - }); - return; - } - - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => { - result.meta.soloMode = true; - $scope.initDashboard(result, $scope); - }); - }; - - $scope.initPanelScope = () => { - const panelInfo = $scope.dashboard.getPanelInfoById(panelId); - - // fake row ctrl scope - $scope.ctrl = { - dashboard: $scope.dashboard, - }; - - $scope.panel = panelInfo.panel; - $scope.panel.soloMode = true; - $scope.$index = 0; - - if (!$scope.panel) { - $scope.appEvent('alert-error', ['Panel not found', '']); - return; - } - }; - - $scope.init(); - } -} - -angular.module('grafana.routes').controller('SoloPanelCtrl', SoloPanelCtrl); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index ebc654f8be9..82763e1839a 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -80,7 +80,6 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.pageIndex = 0; if (this.panel.transform === 'annotations') { - this.setTimeQueryStart(); return this.annotationsSrv .getAnnotations({ dashboard: this.dashboard, diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 3f4b2fb074d..0f4c09a9c77 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -59,10 +59,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/dashboard-solo/:type/:slug', { - templateUrl: 'public/app/features/panel/partials/soloPanel.html', - controller: 'SoloPanelCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', + template: '', + pageClass: 'dashboard-solo', + resolve: { + component: () => SoloPanelPage, + }, }) .when('/dashboard/new', { templateUrl: 'public/app/partials/dashboard.html',