created better error handling (#362)
by using this react-error-boundary package this works also in hooks
This commit is contained in:
parent
3a3bce0955
commit
689e01b461
17 changed files with 151 additions and 46 deletions
|
@ -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",
|
||||
|
|
11
src/App.tsx
11
src/App.tsx
|
@ -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
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
@ -78,6 +82,10 @@ export const App = () => {
|
|||
sidebar={<PageNav />}
|
||||
breadcrumb={<PageBreadCrumbs />}
|
||||
mainContainerId={mainPageContentId}
|
||||
>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorRenderer}
|
||||
onReset={() => (location.href = "/")}
|
||||
>
|
||||
<Switch>
|
||||
{routes(() => {}).map((route, i) => (
|
||||
|
@ -97,6 +105,7 @@ export const App = () => {
|
|||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
</Page>
|
||||
</Router>
|
||||
</AppContexts>
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
"type": "Type",
|
||||
"category": "Category",
|
||||
"priority": "Priority",
|
||||
"unexpectedError": "An unexpected error occurred: '{{error}}'",
|
||||
"retry": "Retry",
|
||||
|
||||
"home": "Home",
|
||||
"manage": "Manage",
|
||||
|
|
|
@ -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 || []);
|
||||
|
||||
|
|
36
src/components/error/ErrorRenderer.tsx
Normal file
36
src/components/error/ErrorRenderer.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
adminClientCall()
|
||||
.then((result) => {
|
||||
try {
|
||||
if (!canceled) {
|
||||
callback(result);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (onError) onError(error);
|
||||
}
|
||||
})
|
||||
.catch(onError);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue