Просмотр исходного кода

Merge pull request #15155 from grafana/solo-panel-rewrite

New react container route for solo panels
Torkel Ödegaard 6 лет назад
Родитель
Сommit
68ae17e4a4

+ 1 - 102
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();
   }
 }
 

+ 123 - 0
public/app/features/dashboard/containers/SoloPanelPage.tsx

@@ -0,0 +1,123 @@
+// 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';
+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;
+  urlUid?: string;
+  urlSlug?: string;
+  urlType?: string;
+  $scope: any;
+  $injector: any;
+  updateLocation: typeof updateLocation;
+}
+
+interface State {
+  panel: PanelModel | null;
+  dashboard: DashboardModel | null;
+  notFound: boolean;
+}
+
+export class SoloPanelPage extends Component<Props, State> {
+
+  state: State = {
+    panel: null,
+    dashboard: null,
+    notFound: false,
+  };
+
+  componentDidMount() {
+    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(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;
+
+    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 (
+        <div className="alert alert-error">
+          Panel with id { panelId } not found
+        </div>
+      );
+    }
+
+    if (!panel) {
+      return <div>Loading & initializing dashboard</div>;
+    }
+
+    return (
+      <div className="panel-solo">
+        <DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} />
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: StoreState) => ({
+  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));

+ 0 - 3
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -135,11 +135,8 @@ export class DataPanel extends Component<Props, State> {
         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;
       }

+ 8 - 1
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<Props, State> {
     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 (
       <div className="panel-content">
         <PanelComponent

+ 1 - 1
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;

+ 0 - 1
public/app/features/panel/all.ts

@@ -1,6 +1,5 @@
 import './panel_header';
 import './panel_directive';
-import './solo_panel_ctrl';
 import './query_ctrl';
 import './panel_editor_tab';
 import './query_editor_row';

+ 0 - 11
public/app/features/panel/metrics_panel_ctrl.ts

@@ -16,7 +16,6 @@ class MetricsPanelCtrl extends PanelCtrl {
   datasourceSrv: any;
   timeSrv: any;
   templateSrv: any;
-  timing: any;
   range: any;
   interval: any;
   intervalMs: any;
@@ -81,7 +80,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.loading = true;
 
     // load datasource service
-    this.setTimeQueryStart();
     this.datasourceSrv
       .get(this.panel.datasource)
       .then(this.updateTimeRange.bind(this))
@@ -112,14 +110,6 @@ class MetricsPanelCtrl extends PanelCtrl {
       });
   }
 
-  setTimeQueryStart() {
-    this.timing.queryStart = new Date().getTime();
-  }
-
-  setTimeQueryEnd() {
-    this.timing.queryEnd = new Date().getTime();
-  }
-
   updateTimeRange(datasource?) {
     this.datasource = datasource || this.datasource;
     this.range = this.timeSrv.timeRange();
@@ -181,7 +171,6 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
 
   handleQueryResult(result) {
-    this.setTimeQueryEnd();
     this.loading = false;
 
     // check for if data source returns subject

+ 6 - 19
public/app/features/panel/panel_ctrl.ts

@@ -1,5 +1,4 @@
 import _ from 'lodash';
-import $ from 'jquery';
 import Remarkable from 'remarkable';
 
 import config from 'app/core/config';
@@ -13,7 +12,7 @@ import {
   sharePanel as sharePanelUtil,
 } from 'app/features/dashboard/utils/panel';
 
-import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
+import { GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
 
 export class PanelCtrl {
   panel: any;
@@ -31,8 +30,8 @@ export class PanelCtrl {
   height: any;
   containerHeight: any;
   events: Emitter;
-  timing: any;
   loading: boolean;
+  timing: any;
   maxPanelsPerRowOptions: number[];
 
   constructor($scope, $injector) {
@@ -42,7 +41,7 @@ export class PanelCtrl {
     this.$timeout = $injector.get('$timeout');
     this.editorTabs = [];
     this.events = this.panel.events;
-    this.timing = {};
+    this.timing = {}; // not used but here to not break plugins
 
     const plugin = config.panels[this.panel.type];
     if (plugin) {
@@ -59,7 +58,7 @@ export class PanelCtrl {
   }
 
   renderingCompleted() {
-    profiler.renderingCompleted(this.panel.id, this.timing);
+    profiler.renderingCompleted(this.panel.id);
   }
 
   refresh() {
@@ -200,24 +199,12 @@ export class PanelCtrl {
     return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
   }
 
-  calculatePanelHeight() {
-    if (this.panel.isEditing) {
-      this.containerHeight = $('.panel-wrapper--edit').height();
-    } else if (this.panel.fullscreen)  {
-      this.containerHeight = $('.panel-wrapper--view').height();
-    } else {
-      this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
-    }
-
-    if (this.panel.soloMode) {
-      this.containerHeight = $(window).height();
-    }
-
+  calculatePanelHeight(containerHeight) {
+    this.containerHeight = containerHeight;
     this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT);
   }
 
   render(payload?) {
-    this.timing.renderStart = new Date().getTime();
     this.events.emit('render', payload);
   }
 

+ 8 - 6
public/app/features/panel/panel_directive.ts

@@ -101,7 +101,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
       });
 
       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;

+ 0 - 4
public/app/features/panel/partials/soloPanel.html

@@ -1,4 +0,0 @@
-<div class="panel-solo" ng-if="panel">
-	<plugin-component type="panel">
-	</plugin-component>
-</div>

+ 0 - 58
public/app/features/panel/solo_panel_ctrl.ts

@@ -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);

+ 0 - 1
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,

+ 7 - 1
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]);
 

+ 11 - 8
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,16 +52,18 @@ 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: '<react-container />',
+      pageClass: 'dashboard-solo',
+      resolve: {
+        component: () => SoloPanelPage,
+      },
     })
     .when('/dashboard-solo/:type/:slug', {
-      templateUrl: 'public/app/features/panel/partials/soloPanel.html',
-      controller: 'SoloPanelCtrl',
-      reloadOnSearch: false,
-      pageClass: 'page-dashboard',
+      template: '<react-container />',
+      pageClass: 'dashboard-solo',
+      resolve: {
+        component: () => SoloPanelPage,
+      },
     })
     .when('/dashboard/new', {
       templateUrl: 'public/app/partials/dashboard.html',

+ 7 - 0
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;