From 2771fbe62425acd7091e5df8ed713bbba7d6f804 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 1 Oct 2019 14:24:18 -0300 Subject: [PATCH 1/8] Initial React Rendering with useDashboard --- .../components/dashboards/DashboardGrid.jsx | 2 +- .../pages/dashboards/PublicDashboardPage.jsx | 115 ++++++++++++++++++ .../pages/dashboards/PublicDashboardPage.less | 16 +++ .../pages/dashboards/public-dashboard-page.js | 2 +- client/app/pages/dashboards/useDashboard.js | 60 +++++++++ client/app/services/dashboard.js | 5 + 6 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 client/app/pages/dashboards/PublicDashboardPage.jsx create mode 100644 client/app/pages/dashboards/PublicDashboardPage.less create mode 100644 client/app/pages/dashboards/useDashboard.js diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index c16c85e65d..47b319dd8d 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -34,7 +34,7 @@ const WidgetType = PropTypes.shape({ const SINGLE = 'single-column'; const MULTI = 'multi-column'; -class DashboardGrid extends React.Component { +export class DashboardGrid extends React.Component { static propTypes = { isEditing: PropTypes.bool.isRequired, isPublic: PropTypes.bool, diff --git a/client/app/pages/dashboards/PublicDashboardPage.jsx b/client/app/pages/dashboards/PublicDashboardPage.jsx new file mode 100644 index 0000000000..64e1b867c3 --- /dev/null +++ b/client/app/pages/dashboards/PublicDashboardPage.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { isEmpty } from 'lodash'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; +import { BigMessage } from '@/components/BigMessage'; +import { PageHeader } from '@/components/PageHeader'; +import { Parameters } from '@/components/Parameters'; +import { DashboardGrid } from '@/components/dashboards/DashboardGrid'; +import { Filters } from '@/components/Filters'; +import { Dashboard } from '@/services/dashboard'; +import { $route as ngRoute } from '@/services/ng'; +import PromiseRejectionError from '@/lib/promise-rejection-error'; +import logoUrl from '@/assets/images/redash_icon_small.png'; +import useDashboard from './useDashboard'; + +import './PublicDashboardPage.less'; + + +function PublicDashboard({ dashboard }) { + const { globalParameters, filters, setFilters, refreshDashboard, + widgets, loadWidget, refreshWidget } = useDashboard(dashboard); + + return ( +
+ + {!isEmpty(globalParameters) && ( +
+ +
+ )} + {!isEmpty(filters) && ( +
+ +
+ )} +
+ +
+
+ ); +} + +PublicDashboard.propTypes = { + dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types +}; + +class PublicDashboardPage extends React.Component { + state = { + loading: true, + dashboard: null, + }; + + componentDidMount() { + Dashboard.getByToken({ token: ngRoute.current.params.token }).$promise + .then(dashboard => this.setState({ dashboard, loading: false })) + .catch((error) => { throw new PromiseRejectionError(error); }); + } + + render() { + const { loading, dashboard } = this.state; + return ( +
+ {loading ? ( +
+ +
+ ) : ( + + )} + +
+ ); + } +} + +export default function init(ngModule) { + ngModule.component('publicDashboardPage', react2angular(PublicDashboardPage)); + + function session($route, Auth) { + const token = $route.current.params.token; + Auth.setApiKey(token); + return Auth.loadConfig(); + } + + ngModule.config(($routeProvider) => { + $routeProvider.when('/public/dashboards/:token', { + template: '', + reloadOnSearch: false, + resolve: { + session, + }, + }); + }); + + return []; +} + +init.init = true; diff --git a/client/app/pages/dashboards/PublicDashboardPage.less b/client/app/pages/dashboards/PublicDashboardPage.less new file mode 100644 index 0000000000..b13605a771 --- /dev/null +++ b/client/app/pages/dashboards/PublicDashboardPage.less @@ -0,0 +1,16 @@ +.public-dashboard-page { + > .container { + min-height: calc(100vh - 95px); + } + + .loading-message { + display: flex; + align-items: center; + justify-content: center; + } + + #footer { + height: 95px; + text-align: center; + } +} \ No newline at end of file diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 6152a9ea5e..b2aa917e37 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -95,4 +95,4 @@ export default function init(ngModule) { return []; } -init.init = true; +// init.init = true; diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js new file mode 100644 index 0000000000..60aa126797 --- /dev/null +++ b/client/app/pages/dashboards/useDashboard.js @@ -0,0 +1,60 @@ +import { useState, useEffect, useMemo } from 'react'; +import { isEmpty, includes, compact } from 'lodash'; +import { $location } from '@/services/ng'; +import { collectDashboardFilters } from '@/services/dashboard'; + +function getAffectedWidgets(widgets, updatedParameters = []) { + return !isEmpty(updatedParameters) ? widgets.filter( + widget => Object.values(widget.getParameterMappings()).filter( + ({ type }) => type === 'dashboard-level', + ).some( + ({ mapTo }) => includes(updatedParameters.map(p => p.name), mapTo), + ), + ) : widgets; +} + +function useDashboard(dashboard) { + const [filters, setFilters] = useState([]); + const [widgets, setWidgets] = useState(dashboard.widgets); + const globalParameters = useMemo(() => dashboard.getParametersDefs(), [dashboard]); + + const loadWidget = (widget, forceRefresh = false) => { + widget.getParametersDefs(); // Force widget to read parameters values from URL + setWidgets(dashboard.widgets); + return widget.load(forceRefresh).then((result) => { + setWidgets(dashboard.widgets); + return result; + }); + }; + + const refreshWidget = widget => loadWidget(widget, true); + + const collectFilters = (forceRefresh = false, updatedParameters = []) => { + const affectedWidgets = getAffectedWidgets(widgets, updatedParameters); + const queryResultPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); + + return Promise.all(queryResultPromises).then((queryResults) => { + const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search()); + setFilters(updatedFilters); + }); + }; + + const refreshDashboard = updatedParameters => collectFilters(true, updatedParameters); + const loadDashboard = () => collectFilters(); + + useEffect(() => { + loadDashboard(); + }, []); + + return { + widgets, + globalParameters, + filters, + setFilters, + refreshDashboard, + loadWidget, + refreshWidget, + }; +} + +export default useDashboard; diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js index 509ab387cf..a49543f376 100644 --- a/client/app/services/dashboard.js +++ b/client/app/services/dashboard.js @@ -149,6 +149,11 @@ function DashboardService($resource, $http, $location, currentUser) { { slug: '@slug' }, { get: { method: 'GET', transformResponse: transform }, + getByToken: { + method: 'GET', + url: 'api/dashboards/public/:token', + transformResponse: transform, + }, save: { method: 'POST', transformResponse: transform }, query: { method: 'GET', isArray: false, transformResponse: transform }, recent: { From aa66aff9991e5abc4070b614cdbec6689debbf79 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 1 Oct 2019 17:46:07 -0300 Subject: [PATCH 2/8] Make sure widgets refresh + useCallback --- client/app/pages/dashboards/useDashboard.js | 28 ++++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js index 60aa126797..9de6386376 100644 --- a/client/app/pages/dashboards/useDashboard.js +++ b/client/app/pages/dashboards/useDashboard.js @@ -1,5 +1,5 @@ -import { useState, useEffect, useMemo } from 'react'; -import { isEmpty, includes, compact } from 'lodash'; +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { isEmpty, includes, compact, map } from 'lodash'; import { $location } from '@/services/ng'; import { collectDashboardFilters } from '@/services/dashboard'; @@ -18,33 +18,31 @@ function useDashboard(dashboard) { const [widgets, setWidgets] = useState(dashboard.widgets); const globalParameters = useMemo(() => dashboard.getParametersDefs(), [dashboard]); - const loadWidget = (widget, forceRefresh = false) => { + const loadWidget = useCallback((widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - setWidgets(dashboard.widgets); - return widget.load(forceRefresh).then((result) => { - setWidgets(dashboard.widgets); - return result; - }); - }; + setWidgets([...dashboard.widgets]); // TODO: Explore a better way to do this + return widget.load(forceRefresh).then(() => setWidgets([...dashboard.widgets])); + }, [dashboard]); - const refreshWidget = widget => loadWidget(widget, true); + const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]); - const collectFilters = (forceRefresh = false, updatedParameters = []) => { + const collectFilters = useCallback((forceRefresh = false, updatedParameters = []) => { const affectedWidgets = getAffectedWidgets(widgets, updatedParameters); - const queryResultPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); + const loadWidgetPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); - return Promise.all(queryResultPromises).then((queryResults) => { + return Promise.all(loadWidgetPromises).then(() => { + const queryResults = compact(map(widgets, widget => widget.getQueryResult())); const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search()); setFilters(updatedFilters); }); - }; + }, [dashboard, widgets, loadWidget]); const refreshDashboard = updatedParameters => collectFilters(true, updatedParameters); const loadDashboard = () => collectFilters(); useEffect(() => { loadDashboard(); - }, []); + }, [dashboard]); return { widgets, From 49dffbae64a2e3bb2e9fbd285adb9c7123661304 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 2 Oct 2019 19:38:05 -0300 Subject: [PATCH 3/8] Rename collectFilters and add refreshRate --- client/app/pages/dashboards/useDashboard.js | 24 +++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js index 9de6386376..dbb1c60a3a 100644 --- a/client/app/pages/dashboards/useDashboard.js +++ b/client/app/pages/dashboards/useDashboard.js @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { isEmpty, includes, compact, map } from 'lodash'; +import { isEmpty, isNaN, includes, compact, map } from 'lodash'; import { $location } from '@/services/ng'; import { collectDashboardFilters } from '@/services/dashboard'; @@ -13,20 +13,26 @@ function getAffectedWidgets(widgets, updatedParameters = []) { ) : widgets; } +function getRefreshRateFromUrl() { + const refreshRate = parseFloat($location.search().refresh); + return isNaN(refreshRate) ? null : Math.max(30, refreshRate); +} + function useDashboard(dashboard) { const [filters, setFilters] = useState([]); const [widgets, setWidgets] = useState(dashboard.widgets); const globalParameters = useMemo(() => dashboard.getParametersDefs(), [dashboard]); + const refreshRate = useMemo(getRefreshRateFromUrl, []); const loadWidget = useCallback((widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - setWidgets([...dashboard.widgets]); // TODO: Explore a better way to do this - return widget.load(forceRefresh).then(() => setWidgets([...dashboard.widgets])); + setWidgets([...dashboard.widgets]); + return widget.load(forceRefresh).finally(() => setWidgets([...dashboard.widgets])); }, [dashboard]); const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]); - const collectFilters = useCallback((forceRefresh = false, updatedParameters = []) => { + const loadDashboard = useCallback((forceRefresh = false, updatedParameters = []) => { const affectedWidgets = getAffectedWidgets(widgets, updatedParameters); const loadWidgetPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); @@ -37,13 +43,19 @@ function useDashboard(dashboard) { }); }, [dashboard, widgets, loadWidget]); - const refreshDashboard = updatedParameters => collectFilters(true, updatedParameters); - const loadDashboard = () => collectFilters(); + const refreshDashboard = updatedParameters => loadDashboard(true, updatedParameters); useEffect(() => { loadDashboard(); }, [dashboard]); + useEffect(() => { + if (refreshRate) { + const refreshTimer = setInterval(refreshDashboard, refreshRate * 1000); + return () => clearInterval(refreshTimer); + } + }, [refreshRate]); + return { widgets, globalParameters, From 25166352eb6968248c56ead232e0694b4aa6abce Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 2 Oct 2019 21:09:04 -0300 Subject: [PATCH 4/8] Fix error updates not being rendered --- client/app/pages/dashboards/useDashboard.js | 2 +- client/app/services/dashboard.js | 2 +- client/app/services/widget.js | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js index dbb1c60a3a..6ced1b65ba 100644 --- a/client/app/pages/dashboards/useDashboard.js +++ b/client/app/pages/dashboards/useDashboard.js @@ -36,7 +36,7 @@ function useDashboard(dashboard) { const affectedWidgets = getAffectedWidgets(widgets, updatedParameters); const loadWidgetPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); - return Promise.all(loadWidgetPromises).then(() => { + return Promise.all(loadWidgetPromises).finally(() => { const queryResults = compact(map(widgets, widget => widget.getQueryResult())); const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search()); setFilters(updatedFilters); diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js index a49543f376..86869c5e70 100644 --- a/client/app/services/dashboard.js +++ b/client/app/services/dashboard.js @@ -7,7 +7,7 @@ export let Dashboard = null; // eslint-disable-line import/no-mutable-exports export function collectDashboardFilters(dashboard, queryResults, urlParams) { const filters = {}; _.each(queryResults, (queryResult) => { - const queryFilters = queryResult ? queryResult.getFilters() : []; + const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : []; _.each(queryFilters, (queryFilter) => { const hasQueryStringValue = _.has(urlParams, queryFilter.name); diff --git a/client/app/services/widget.js b/client/app/services/widget.js index 1cd5da4f14..fe720483b6 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -146,14 +146,16 @@ function WidgetFactory($http, $location, Query) { } this.queryResult = this.getQuery().getQueryResult(maxAge); - this.queryResult.toPromise() + return this.queryResult.toPromise() .then((result) => { this.loading = false; this.data = result; + return result; }) .catch((error) => { this.loading = false; this.data = error; + return error; }); } From 6cc5091ee5f77e996f32956417cb0b14ccddfdf7 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 3 Oct 2019 10:08:24 -0300 Subject: [PATCH 5/8] Only render widget bottom when queryResults exists --- .../dashboards/dashboard-widget/VisualizationWidget.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index b965d028d6..5d9d0fb59f 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -123,10 +123,10 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { } }; - return ( + return widgetQueryResult ? ( <> - {(!isPublic && !!widgetQueryResult) && ( + {!isPublic && ( refreshWidget(1)} @@ -162,7 +162,7 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { - ); + ) : null; } VisualizationWidgetFooter.propTypes = { From 779616133f3aef03f36ca6fe9e9b3722d1d8f5ce Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 3 Oct 2019 13:10:21 -0300 Subject: [PATCH 6/8] Cleanup --- client/app/pages/dashboards/dashboard.less | 11 --- .../dashboards/public-dashboard-page.html | 33 ------- .../pages/dashboards/public-dashboard-page.js | 98 ------------------- 3 files changed, 142 deletions(-) delete mode 100644 client/app/pages/dashboards/public-dashboard-page.html delete mode 100644 client/app/pages/dashboards/public-dashboard-page.js diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index 47cd402aac..09b4f0fb3f 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -206,17 +206,6 @@ } } -public-dashboard-page { - > .container { - min-height: calc(100vh - 95px); - } - - #footer { - height: 95px; - text-align: center; - } -} - /**** grid bg - based on 6 cols, 35px rows and 15px spacing ****/ diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html deleted file mode 100644 index 2c1d251d3f..0000000000 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ /dev/null @@ -1,33 +0,0 @@ -
- - -
- -
- -
- -
- -
- -
-
- - - - diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js deleted file mode 100644 index b2aa917e37..0000000000 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ /dev/null @@ -1,98 +0,0 @@ -import PromiseRejectionError from '@/lib/promise-rejection-error'; -import logoUrl from '@/assets/images/redash_icon_small.png'; -import template from './public-dashboard-page.html'; -import dashboardGridOptions from '@/config/dashboard-grid-options'; -import './dashboard.less'; - -function loadDashboard($http, $route) { - const token = $route.current.params.token; - return $http.get(`api/dashboards/public/${token}`).then(response => response.data); -} - -const PublicDashboardPage = { - template, - bindings: { - dashboard: '<', - }, - controller($scope, $timeout, $location, $http, $route, Dashboard) { - 'ngInject'; - - this.filters = []; - - this.dashboardGridOptions = Object.assign({}, dashboardGridOptions, { - resizable: { enabled: false }, - draggable: { enabled: false }, - }); - - this.logoUrl = logoUrl; - this.public = true; - this.globalParameters = []; - - this.extractGlobalParameters = () => { - this.globalParameters = this.dashboard.getParametersDefs(); - }; - - const refreshRate = Math.max(30, parseFloat($location.search().refresh)); - - // ANGULAR_REMOVE_ME This forces Widgets re-rendering - // use state when PublicDashboard is migrated to React - this.forceDashboardGridReload = () => { - this.dashboard.widgets = [...this.dashboard.widgets]; - }; - - this.loadWidget = (widget, forceRefresh = false) => { - widget.getParametersDefs(); // Force widget to read parameters values from URL - this.forceDashboardGridReload(); - return widget.load(forceRefresh).finally(this.forceDashboardGridReload); - }; - - this.refreshWidget = widget => this.loadWidget(widget, true); - - this.refreshDashboard = () => { - loadDashboard($http, $route).then((data) => { - this.dashboard = new Dashboard(data); - this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); - this.dashboard.widgets.forEach(widget => this.loadWidget(widget, !!refreshRate)); - this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters) - this.filtersOnChange = (allFilters) => { - this.filters = allFilters; - $scope.$applyAsync(); - }; - - this.extractGlobalParameters(); - }).catch((error) => { - throw new PromiseRejectionError(error); - }); - - if (refreshRate) { - $timeout(this.refreshDashboard, refreshRate * 1000.0); - } - }; - - this.refreshDashboard(); - }, -}; - -export default function init(ngModule) { - ngModule.component('publicDashboardPage', PublicDashboardPage); - - function session($http, $route, Auth) { - const token = $route.current.params.token; - Auth.setApiKey(token); - return Auth.loadConfig(); - } - - ngModule.config(($routeProvider) => { - $routeProvider.when('/public/dashboards/:token', { - template: '', - reloadOnSearch: false, - resolve: { - session, - }, - }); - }); - - return []; -} - -// init.init = true; From ecd55f44cc19393eb28084a464afe84570012906 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 3 Oct 2019 13:21:19 -0300 Subject: [PATCH 7/8] Add useCallback to refreshDashboard --- client/app/pages/dashboards/useDashboard.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js index 6ced1b65ba..29192636ce 100644 --- a/client/app/pages/dashboards/useDashboard.js +++ b/client/app/pages/dashboards/useDashboard.js @@ -43,7 +43,10 @@ function useDashboard(dashboard) { }); }, [dashboard, widgets, loadWidget]); - const refreshDashboard = updatedParameters => loadDashboard(true, updatedParameters); + const refreshDashboard = useCallback( + updatedParameters => loadDashboard(true, updatedParameters), + [loadDashboard], + ); useEffect(() => { loadDashboard(); From bce0d971e6385954505194625fd3167a342d80fe Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 3 Oct 2019 15:19:37 -0300 Subject: [PATCH 8/8] Make sure Promise.all have all promises done --- client/app/pages/dashboards/useDashboard.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/app/pages/dashboards/useDashboard.js b/client/app/pages/dashboards/useDashboard.js index 29192636ce..287e44f3f6 100644 --- a/client/app/pages/dashboards/useDashboard.js +++ b/client/app/pages/dashboards/useDashboard.js @@ -34,9 +34,11 @@ function useDashboard(dashboard) { const loadDashboard = useCallback((forceRefresh = false, updatedParameters = []) => { const affectedWidgets = getAffectedWidgets(widgets, updatedParameters); - const loadWidgetPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh))); + const loadWidgetPromises = compact( + affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error)), + ); - return Promise.all(loadWidgetPromises).finally(() => { + return Promise.all(loadWidgetPromises).then(() => { const queryResults = compact(map(widgets, widget => widget.getQueryResult())); const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search()); setFilters(updatedFilters);