Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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