created better error handling (#362)

by using this react-error-boundary package this works also in hooks
This commit is contained in:
Erik Jan de Wit 2021-02-17 22:12:25 +01:00 committed by GitHub
parent 3a3bce0955
commit 689e01b461
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 151 additions and 46 deletions

View file

@ -4,7 +4,9 @@
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": ["tests"],
"workspaces": [
"tests"
],
"scripts": {
"build": "snowpack build",
"build-storybook": "build-storybook -s public",
@ -31,6 +33,7 @@
"moment": "^2.29.1",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"react-error-boundary": "^3.1.0",
"react-hook-form": "^6.8.3",
"react-i18next": "^11.7.0",
"react-router-dom": "^5.2.0",

View file

@ -6,6 +6,7 @@ import {
Switch,
useParams,
} from "react-router-dom";
import { ErrorBoundary, useErrorHandler } from "react-error-boundary";
import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
@ -21,6 +22,7 @@ import { ForbiddenSection } from "./ForbiddenSection";
import { SubGroups } from "./groups/GroupsSection";
import { useRealm } from "./context/realm-context/RealmContext";
import { useAdminClient, asyncStateFetch } from "./context/auth/AdminClient";
import { ErrorRenderer } from "./components/error/ErrorRenderer";
// This must match the id given as scrollableSelector in scroll-form
const mainPageContentId = "kc-main-content-page-container";
@ -42,6 +44,7 @@ const RealmPathSelector = ({ children }: { children: ReactNode }) => {
const { setRealm } = useRealm();
const { realm } = useParams<{ realm: string }>();
const adminClient = useAdminClient();
const handleError = useErrorHandler();
useEffect(
() =>
asyncStateFetch(
@ -50,7 +53,8 @@ const RealmPathSelector = ({ children }: { children: ReactNode }) => {
if (realms.findIndex((r) => r.realm == realm) !== -1) {
setRealm(realm);
}
}
},
handleError
),
[]
);
@ -79,24 +83,29 @@ export const App = () => {
breadcrumb={<PageBreadCrumbs />}
mainContainerId={mainPageContentId}
>
<Switch>
{routes(() => {}).map((route, i) => (
<Route
exact={
route.matchOptions?.exact === undefined
? true
: route.matchOptions.exact
}
key={i}
path={route.path}
component={() => (
<RealmPathSelector>
<SecuredRoute route={route} />
</RealmPathSelector>
)}
/>
))}
</Switch>
<ErrorBoundary
FallbackComponent={ErrorRenderer}
onReset={() => (location.href = "/")}
>
<Switch>
{routes(() => {}).map((route, i) => (
<Route
exact={
route.matchOptions?.exact === undefined
? true
: route.matchOptions.exact
}
key={i}
path={route.path}
component={() => (
<RealmPathSelector>
<SecuredRoute route={route} />
</RealmPathSelector>
)}
/>
))}
</Switch>
</ErrorBoundary>
</Page>
</Router>
</AppContexts>

View file

@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import { useErrorHandler } from "react-error-boundary";
import {
FormGroup,
PageSection,
@ -35,6 +36,7 @@ import { FormAccess } from "../../components/form-access/FormAccess";
export const RoleMappingForm = () => {
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const history = useHistory();
const { addAlert } = useAlerts();
@ -74,7 +76,8 @@ export const RoleMappingForm = () => {
);
return filteredClients;
},
(filteredClients) => setClients(filteredClients)
(filteredClients) => setClients(filteredClients),
handleError
);
}, []);
@ -91,7 +94,8 @@ export const RoleMappingForm = () => {
return await adminClient.roles.find();
}
},
(clientRoles) => setClientRoles(clientRoles)
(clientRoles) => setClientRoles(clientRoles),
handleError
);
}, [selectedClient]);

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
ActionGroup,
AlertVariant,
@ -43,6 +44,7 @@ type Params = {
export const MappingDetails = () => {
const { t } = useTranslation("client-scopes");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const { scopeId, id } = useParams<Params>();
@ -90,7 +92,8 @@ export const MappingDetails = () => {
(result) => {
setConfigProperties(result.configProperties);
setMapping(result.mapping);
}
},
handleError
);
}, []);

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useErrorHandler } from "react-error-boundary";
import {
ActionGroup,
AlertVariant,
@ -41,6 +42,7 @@ export const ClientScopeForm = () => {
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>();
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const providers = useLoginProviders();
const { id } = useParams<{ id: string }>();
@ -67,7 +69,8 @@ export const ClientScopeForm = () => {
return data;
}
},
(data) => setClientScope(data)
(data) => setClientScope(data),
handleError
);
}, [key]);

View file

@ -8,9 +8,10 @@ import {
Tab,
TabTitleText,
} from "@patternfly/react-core";
import { useParams } from "react-router-dom";
import { useErrorHandler } from "react-error-boundary";
import { useTranslation } from "react-i18next";
import { Controller, useForm, useWatch } from "react-hook-form";
import { useParams } from "react-router-dom";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ClientSettings } from "./ClientSettings";
@ -96,6 +97,8 @@ const ClientDetailHeader = ({
export const ClientDetails = () => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const form = useForm();
@ -164,7 +167,8 @@ export const ClientDetails = () => {
(fetchedClient) => {
setClient(fetchedClient);
setupForm(fetchedClient);
}
},
handleError
);
}, []);

View file

@ -1,3 +1,7 @@
import React, { useEffect, useState } from "react";
import { Controller, UseFormMethods, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
ActionGroup,
AlertVariant,
@ -14,18 +18,16 @@ import {
SplitItem,
} from "@patternfly/react-core";
import CredentialRepresentation from "keycloak-admin/lib/defs/credentialRepresentation";
import React, { useEffect, useState } from "react";
import { Controller, UseFormMethods, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import {
useAdminClient,
asyncStateFetch,
} from "../../context/auth/AdminClient";
import { ClientSecret } from "./ClientSecret";
import { SignedJWT } from "./SignedJWT";
import { X509 } from "./X509";
@ -53,6 +55,7 @@ export type CredentialsProps = {
export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const clientAuthenticatorType = useWatch({
control: form.control,
@ -84,7 +87,8 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
({ providers, secret }) => {
setProviders(providers);
setSecret(secret);
}
},
handleError
);
}, []);

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
IFormatter,
IFormatterValueType,
@ -123,6 +124,7 @@ type TableRow = {
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const [searchToggle, setSearchToggle] = useState(false);
@ -182,7 +184,8 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
({ rows, rest }) => {
setRows(rows);
setRest(rest);
}
},
handleError
);
}, [key]);

View file

@ -1,5 +1,6 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
ClipboardCopy,
EmptyState,
@ -145,10 +146,12 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
const tabContent2 = useRef(null);
const tabContent3 = useRef(null);
const handleError = useErrorHandler();
useEffect(() => {
return asyncStateFetch(
() => adminClient.clients.listOptionalClientScopes({ id: clientId }),
(optionalClientScopes) => setSelectableScopes(optionalClientScopes)
(optionalClientScopes) => setSelectableScopes(optionalClientScopes),
handleError
);
}, []);
@ -182,7 +185,8 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
return user;
})
.map((user) => <SelectOption key={user.id} value={user} />)
)
),
handleError
);
}, [userSearch]);
@ -221,7 +225,8 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
setProtocolMappers(mapperList);
refresh();
}
},
handleError
);
}, [selected]);
@ -241,7 +246,8 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
},
(accessToken) => {
setAccessToken(JSON.stringify(accessToken, undefined, 3));
}
},
handleError
);
}, [user, selected]);

View file

@ -50,6 +50,8 @@
"type": "Type",
"category": "Category",
"priority": "Priority",
"unexpectedError": "An unexpected error occurred: '{{error}}'",
"retry": "Retry",
"home": "Home",
"manage": "Manage",

View file

@ -1,5 +1,7 @@
import React, { DependencyList, useEffect, useState } from "react";
import { Spinner } from "@patternfly/react-core";
import { useErrorHandler } from "react-error-boundary";
import { asyncStateFetch } from "../../context/auth/AdminClient";
type DataLoaderProps<T> = {
@ -10,11 +12,13 @@ type DataLoaderProps<T> = {
export function DataLoader<T>(props: DataLoaderProps<T>) {
const [data, setData] = useState<T | undefined>();
const handleError = useErrorHandler();
useEffect(() => {
return asyncStateFetch(
() => props.loader(),
(result) => setData(result)
(result) => setData(result),
handleError
);
}, props.deps || []);

View file

@ -0,0 +1,36 @@
import {
Alert,
AlertActionCloseButton,
AlertActionLink,
AlertVariant,
PageSection,
} from "@patternfly/react-core";
import React from "react";
import { FallbackProps } from "react-error-boundary";
import { useTranslation } from "react-i18next";
export const ErrorRenderer = ({ error, resetErrorBoundary }: FallbackProps) => {
const { t } = useTranslation();
return (
<PageSection>
<Alert
isInline
variant={AlertVariant.danger}
title={error.message}
actionClose={
<AlertActionCloseButton
title={error.message}
onClose={resetErrorBoundary}
/>
}
actionLinks={
<React.Fragment>
<AlertActionLink onClick={resetErrorBoundary}>
{t("retry")}
</AlertActionLink>
</React.Fragment>
}
></Alert>
</PageSection>
);
};

View file

@ -1,5 +1,6 @@
import React, { ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
IAction,
IActions,
@ -139,6 +140,7 @@ export function KeycloakDataTable<T>({
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const handleError = useErrorHandler();
useEffect(() => {
setRefresher && setRefresher(refresh);
@ -168,7 +170,8 @@ export function KeycloakDataTable<T>({
setRows(result);
setFilteredData(result);
setLoading(false);
}
},
handleError
);
}, [key, first, max]);

View file

@ -32,18 +32,26 @@ export const useAdminClient = () => {
*
* @param adminClientCall use this to do your adminClient call
* @param callback when the data is fetched this is where you set your state
* @param onError custom error handler
*/
export function asyncStateFetch<T>(
adminClientCall: () => Promise<T>,
callback: (param: T) => void
callback: (param: T) => void,
onError?: (error: Error) => void
) {
let canceled = false;
adminClientCall().then((result) => {
if (!canceled) {
callback(result);
}
});
adminClientCall()
.then((result) => {
try {
if (!canceled) {
callback(result);
}
} catch (error) {
if (onError) onError(error);
}
})
.catch(onError);
return () => {
canceled = true;

View file

@ -1,4 +1,5 @@
import React, { useContext, useEffect, useState } from "react";
import { useErrorHandler } from "react-error-boundary";
import i18n from "../../i18n";
import { AdminClient, asyncStateFetch } from "../auth/AdminClient";
@ -62,6 +63,7 @@ export const WhoAmIContext = React.createContext<WhoAmIProps>({
type WhoAmIProviderProps = { children: React.ReactNode };
export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
const adminClient = useContext(AdminClient)!;
const handleError = useErrorHandler();
const { realm, setRealm } = useContext(RealmContext);
const [whoAmI, setWhoAmI] = useState<WhoAmI>(new WhoAmI());
const [key, setKey] = useState(0);
@ -78,7 +80,8 @@ export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
setRealm(whoAmI.getHomeRealm());
}
setWhoAmI(whoAmI);
}
},
handleError
);
}, [key]);

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import { useErrorHandler } from "react-error-boundary";
import {
ActionGroup,
AlertVariant,
@ -119,6 +120,7 @@ const requireSslTypes = ["all", "external", "none"];
export const RealmSettingsSection = () => {
const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { realm: realmName } = useRealm();
const { addAlert } = useAlerts();
const { register, control, getValues, setValue, handleSubmit } = useForm();
@ -134,7 +136,8 @@ export const RealmSettingsSection = () => {
(realm) => {
setRealm(realm);
setupForm(realm);
}
},
handleError
);
}, []);

View file

@ -16880,6 +16880,13 @@ react-element-to-jsx-string@^14.0.2, react-element-to-jsx-string@^14.3.1:
"@base2/pretty-print-object" "1.0.0"
is-plain-object "3.0.0"
react-error-boundary@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.0.tgz#9487443df2f9ba1db90d8ab52351814907ea4af3"
integrity sha512-lmPrdi5SLRJR+AeJkqdkGlW/CRkAUvZnETahK58J4xb5wpbfDngasEGu+w0T1iXEhVrYBJZeW+c4V1hILCnMWQ==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.7:
version "6.0.7"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"