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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,10 @@ import {
Tab, Tab,
TabTitleText, TabTitleText,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useParams } from "react-router-dom";
import { useErrorHandler } from "react-error-boundary";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useForm, useWatch } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
import { useParams } from "react-router-dom";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ClientSettings } from "./ClientSettings"; import { ClientSettings } from "./ClientSettings";
@ -96,6 +97,8 @@ const ClientDetailHeader = ({
export const ClientDetails = () => { export const ClientDetails = () => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const form = useForm(); const form = useForm();
@ -164,7 +167,8 @@ export const ClientDetails = () => {
(fetchedClient) => { (fetchedClient) => {
setClient(fetchedClient); setClient(fetchedClient);
setupForm(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 { import {
ActionGroup, ActionGroup,
AlertVariant, AlertVariant,
@ -14,18 +18,16 @@ import {
SplitItem, SplitItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import CredentialRepresentation from "keycloak-admin/lib/defs/credentialRepresentation"; 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 { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form-access/FormAccess"; import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { import {
useAdminClient, useAdminClient,
asyncStateFetch, asyncStateFetch,
} from "../../context/auth/AdminClient"; } from "../../context/auth/AdminClient";
import { ClientSecret } from "./ClientSecret"; import { ClientSecret } from "./ClientSecret";
import { SignedJWT } from "./SignedJWT"; import { SignedJWT } from "./SignedJWT";
import { X509 } from "./X509"; import { X509 } from "./X509";
@ -53,6 +55,7 @@ export type CredentialsProps = {
export const Credentials = ({ clientId, form, save }: CredentialsProps) => { export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const clientAuthenticatorType = useWatch({ const clientAuthenticatorType = useWatch({
control: form.control, control: form.control,
@ -84,7 +87,8 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
({ providers, secret }) => { ({ providers, secret }) => {
setProviders(providers); setProviders(providers);
setSecret(secret); setSecret(secret);
} },
handleError
); );
}, []); }, []);

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
import React, { DependencyList, useEffect, useState } from "react"; import React, { DependencyList, useEffect, useState } from "react";
import { Spinner } from "@patternfly/react-core"; import { Spinner } from "@patternfly/react-core";
import { useErrorHandler } from "react-error-boundary";
import { asyncStateFetch } from "../../context/auth/AdminClient"; import { asyncStateFetch } from "../../context/auth/AdminClient";
type DataLoaderProps<T> = { type DataLoaderProps<T> = {
@ -10,11 +12,13 @@ type DataLoaderProps<T> = {
export function DataLoader<T>(props: DataLoaderProps<T>) { export function DataLoader<T>(props: DataLoaderProps<T>) {
const [data, setData] = useState<T | undefined>(); const [data, setData] = useState<T | undefined>();
const handleError = useErrorHandler();
useEffect(() => { useEffect(() => {
return asyncStateFetch( return asyncStateFetch(
() => props.loader(), () => props.loader(),
(result) => setData(result) (result) => setData(result),
handleError
); );
}, props.deps || []); }, 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 React, { ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import { import {
IAction, IAction,
IActions, IActions,
@ -139,6 +140,7 @@ export function KeycloakDataTable<T>({
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
const handleError = useErrorHandler();
useEffect(() => { useEffect(() => {
setRefresher && setRefresher(refresh); setRefresher && setRefresher(refresh);
@ -168,7 +170,8 @@ export function KeycloakDataTable<T>({
setRows(result); setRows(result);
setFilteredData(result); setFilteredData(result);
setLoading(false); setLoading(false);
} },
handleError
); );
}, [key, first, max]); }, [key, first, max]);

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useErrorHandler } from "react-error-boundary";
import { import {
ActionGroup, ActionGroup,
AlertVariant, AlertVariant,
@ -119,6 +120,7 @@ const requireSslTypes = ["all", "external", "none"];
export const RealmSettingsSection = () => { export const RealmSettingsSection = () => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { realm: realmName } = useRealm(); const { realm: realmName } = useRealm();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const { register, control, getValues, setValue, handleSubmit } = useForm(); const { register, control, getValues, setValue, handleSubmit } = useForm();
@ -134,7 +136,8 @@ export const RealmSettingsSection = () => {
(realm) => { (realm) => {
setRealm(realm); setRealm(realm);
setupForm(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" "@base2/pretty-print-object" "1.0.0"
is-plain-object "3.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: react-error-overlay@^6.0.7:
version "6.0.7" version "6.0.7"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"