Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Public Dashboard Page to React #4203

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion client/app/components/dashboards/DashboardGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
115 changes: 115 additions & 0 deletions client/app/pages/dashboards/PublicDashboardPage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container p-t-10 p-b-20">
<PageHeader title={dashboard.name} />
{!isEmpty(globalParameters) && (
<div className="m-b-10 p-15 bg-white tiled">
<Parameters parameters={globalParameters} onValuesChange={refreshDashboard} />
</div>
)}
{!isEmpty(filters) && (
<div className="m-b-10 p-15 bg-white tiled">
<Filters filters={filters} onChange={setFilters} />
</div>
)}
<div id="dashboard-container">
<DashboardGrid
dashboard={dashboard}
widgets={widgets}
filters={filters}
isEditing={false}
isPublic
onLoadWidget={loadWidget}
onRefreshWidget={refreshWidget}
/>
</div>
</div>
);
}

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 (
<div className="public-dashboard-page">
{loading ? (
<div className="container loading-message">
<BigMessage
className=""
icon="fa-spinner fa-2x fa-pulse"
message="Loading..."
/>
</div>
) : (
<PublicDashboard dashboard={dashboard} />
)}
<div id="footer">
<div className="text-center">
<a href="https://redash.io"><img alt="Redash Logo" src={logoUrl} width="38" /></a>
</div>
Powered by <a href="https://redash.io/?ref=public-dashboard">Redash</a>
</div>
</div>
);
}
}

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: '<public-dashboard-page></public-dashboard-page>',
reloadOnSearch: false,
resolve: {
session,
},
});
});

return [];
}

init.init = true;
16 changes: 16 additions & 0 deletions client/app/pages/dashboards/PublicDashboardPage.less
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion client/app/pages/dashboards/public-dashboard-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ export default function init(ngModule) {
return [];
}

init.init = true;
// init.init = true;
70 changes: 70 additions & 0 deletions client/app/pages/dashboards/useDashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { isEmpty, isNaN, includes, compact, map } 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 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]);
return widget.load(forceRefresh).finally(() => setWidgets([...dashboard.widgets]));
}, [dashboard]);

const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]);

const loadDashboard = useCallback((forceRefresh = false, updatedParameters = []) => {
const affectedWidgets = getAffectedWidgets(widgets, updatedParameters);
const loadWidgetPromises = compact(affectedWidgets.map(widget => loadWidget(widget, forceRefresh)));

return Promise.all(loadWidgetPromises).finally(() => {
const queryResults = compact(map(widgets, widget => widget.getQueryResult()));
const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search());
setFilters(updatedFilters);
});
}, [dashboard, widgets, loadWidget]);

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,
filters,
setFilters,
refreshDashboard,
loadWidget,
refreshWidget,
};
}

export default useDashboard;
7 changes: 6 additions & 1 deletion client/app/services/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 3 additions & 1 deletion client/app/services/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Comment on lines +149 to 159
Copy link
Member Author

@gabrieldutra gabrieldutra Oct 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has been playing around with me for a while (in here and in the Widget Migration).

image

It was returning the queryResult before updating this.data, which was a weird result when I expected the widgets to render an error and they were not doing it

}

Expand Down