Skip to content

Commit

Permalink
Refine routes definitions (#4579)
Browse files Browse the repository at this point in the history
* Refine routes definitions

* Replace HoC wrappers with functions to create route definition

* Some updates for code consistency

* ItemsList component: remove currentRoute dependency

* Prepare route parametes in wrapper functions
  • Loading branch information
kravets-levko authored and arikfr committed Jan 26, 2020
1 parent a36b101 commit 19c6d33
Show file tree
Hide file tree
Showing 30 changed files with 317 additions and 630 deletions.
59 changes: 0 additions & 59 deletions client/app/components/ApplicationArea/AuthenticatedPageWrapper.jsx

This file was deleted.

41 changes: 0 additions & 41 deletions client/app/components/ApplicationArea/SignedOutPageWrapper.jsx

This file was deleted.

63 changes: 63 additions & 0 deletions client/app/components/ApplicationArea/routeWithApiKeySession.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useEffect, useState, useContext } from "react";
import PropTypes from "prop-types";
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { Auth } from "@/services/auth";

// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary
// - `apiKey` field

function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { handleError } = useContext(ErrorBoundaryContext);

useEffect(() => {
let isCancelled = false;
Auth.setApiKey(apiKey);
Auth.loadConfig()
.then(() => {
if (!isCancelled) {
setIsAuthenticated(true);
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, [apiKey]);

if (!isAuthenticated) {
return null;
}

return (
<React.Fragment key={currentRoute.key}>
{renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError, apiKey })}
</React.Fragment>
);
}

ApiKeySessionWrapper.propTypes = {
apiKey: PropTypes.string.isRequired,
renderChildren: PropTypes.func,
};

ApiKeySessionWrapper.defaultProps = {
renderChildren: () => null,
};

export default function routeWithApiKeySession({ render, getApiKey, ...rest }) {
return {
...rest,
render: currentRoute => (
<ApiKeySessionWrapper apiKey={getApiKey(currentRoute)} currentRoute={currentRoute} renderChildren={render} />
),
};
}
82 changes: 82 additions & 0 deletions client/app/components/ApplicationArea/routeWithUserSession.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import ErrorBoundary, { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { Auth } from "@/services/auth";
import organizationStatus from "@/services/organizationStatus";
import ApplicationHeader from "./ApplicationHeader";
import ErrorMessage from "./ErrorMessage";

// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary

function UserSessionWrapper({ bodyClass, currentRoute, renderChildren }) {
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());

useEffect(() => {
let isCancelled = false;
Promise.all([Auth.requireSession(), organizationStatus.refresh()])
.then(() => {
if (!isCancelled) {
setIsAuthenticated(!!Auth.isAuthenticated());
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, []);

useEffect(() => {
if (bodyClass) {
document.body.classList.toggle(bodyClass, true);
return () => {
document.body.classList.toggle(bodyClass, false);
};
}
}, [bodyClass]);

if (!isAuthenticated) {
return null;
}

return (
<React.Fragment>
<ApplicationHeader />
<React.Fragment key={currentRoute.key}>
<ErrorBoundary renderError={error => <ErrorMessage error={error} />}>
<ErrorBoundaryContext.Consumer>
{({ handleError }) =>
renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError })
}
</ErrorBoundaryContext.Consumer>
</ErrorBoundary>
</React.Fragment>
</React.Fragment>
);
}

UserSessionWrapper.propTypes = {
bodyClass: PropTypes.string,
renderChildren: PropTypes.func,
};

UserSessionWrapper.defaultProps = {
bodyClass: null,
renderChildren: () => null,
};

export default function routeWithUserSession({ render, bodyClass, ...rest }) {
return {
...rest,
render: currentRoute => (
<UserSessionWrapper bodyClass={bodyClass} currentRoute={currentRoute} renderChildren={render} />
),
};
}
5 changes: 4 additions & 1 deletion client/app/components/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const logger = debug("redash:errors");

export const ErrorBoundaryContext = React.createContext({
handleError: error => {
throw error;
// Allow calling chain to roll up, and then throw the error in global context
setTimeout(() => {
throw error;
});
},
reset: () => {},
});
Expand Down
26 changes: 10 additions & 16 deletions client/app/components/items-list/ItemsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import hoistNonReactStatics from "hoist-non-react-statics";
import { clientConfig } from "@/services/auth";

export const ControllerType = PropTypes.shape({
// values of props declared by wrapped component, current route's locals (`resolve: { ... }`) and title
// values of props declared by wrapped component and some additional props from items list
params: PropTypes.object.isRequired,

isLoaded: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -38,13 +38,11 @@ export const ControllerType = PropTypes.shape({
export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
class ItemsListWrapper extends React.Component {
static propTypes = {
...omit(WrappedComponent.propTypes, ["controller"]),
onError: PropTypes.func,
children: PropTypes.node,
};

static defaultProps = {
...omit(WrappedComponent.defaultProps, ["controller"]),
onError: error => {
// Allow calling chain to roll up, and then throw the error in global context
setTimeout(() => {
Expand Down Expand Up @@ -102,20 +100,13 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {

// eslint-disable-next-line class-methods-use-this
getState({ isLoaded, totalCount, pageItems, params, ...rest }) {
params = {
// Custom params from items source
...params,

title: this.props.currentRoute.title,
...this.props.routeParams,

// Add to params all props except of own ones
...omit(this.props, ["onError", "children", "currentRoute", "routeParams"]),
};
return {
...rest,

params,
params: {
...params, // custom params from items source
...omit(this.props, ["onError", "children"]), // add all props except of own ones
},

isLoaded,
isEmpty: !isLoaded || totalCount === 0,
Expand All @@ -128,8 +119,11 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
render() {
// don't pass own props to wrapped component
const { children, onError, ...props } = this.props;
props.controller = this.state;
return <WrappedComponent {...props}>{children}</WrappedComponent>;
return (
<WrappedComponent {...props} controller={this.state}>
{children}
</WrappedComponent>
);
}
}

Expand Down
15 changes: 4 additions & 11 deletions client/app/pages/admin/Jobs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { axios } from "@/services/axios";
import Alert from "antd/lib/alert";
import Tabs from "antd/lib/tabs";
import * as Grid from "antd/lib/grid";
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
import Layout from "@/components/admin/Layout";
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { CounterCard, WorkersTable, QueuesTable, OtherJobsTable } from "@/components/admin/RQStatus";

import location from "@/services/location";
Expand Down Expand Up @@ -121,14 +120,8 @@ class Jobs extends React.Component {
}
}

export default {
export default routeWithUserSession({
path: "/admin/queries/jobs",
title: "RQ Status",
render: currentRoute => (
<AuthenticatedPageWrapper key={currentRoute.key}>
<ErrorBoundaryContext.Consumer>
{({ handleError }) => <Jobs {...currentRoute.routeParams} onError={handleError} />}
</ErrorBoundaryContext.Consumer>
</AuthenticatedPageWrapper>
),
};
render: pageProps => <Jobs {...pageProps} />,
});
21 changes: 4 additions & 17 deletions client/app/pages/admin/OutdatedQueries.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { axios } from "@/services/axios";

import Switch from "antd/lib/switch";
import * as Grid from "antd/lib/grid";
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
import Paginator from "@/components/Paginator";
import { QueryTagsControl } from "@/components/tags-control/TagsControl";
import SchedulePhrase from "@/components/queries/SchedulePhrase";
import TimeAgo from "@/components/TimeAgo";
import Layout from "@/components/admin/Layout";
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";

import { wrap as itemsList, ControllerType } from "@/components/items-list/ItemsList";
import { ItemsSource } from "@/components/items-list/classes/ItemsSource";
Expand Down Expand Up @@ -170,20 +169,8 @@ const OutdatedQueriesPage = itemsList(
() => new StateStorage({ orderByField: "created_at", orderByReverse: true })
);

export default {
export default routeWithUserSession({
path: "/admin/queries/outdated",
title: "Outdated Queries",
render: currentRoute => (
<AuthenticatedPageWrapper key={currentRoute.key}>
<ErrorBoundaryContext.Consumer>
{({ handleError }) => (
<OutdatedQueriesPage
routeParams={{ ...currentRoute.routeParams, currentPage: "outdated_queries" }}
currentRoute={currentRoute}
onError={handleError}
/>
)}
</ErrorBoundaryContext.Consumer>
</AuthenticatedPageWrapper>
),
};
render: pageProps => <OutdatedQueriesPage {...pageProps} currentPage="outdated_queries" />,
});
Loading

0 comments on commit 19c6d33

Please sign in to comment.