From 7f21d03bc2d0bb771e702546a8a69d3d53a33cd1 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 29 Apr 2021 08:28:59 +0200 Subject: [PATCH 1/3] changed `asyncStateFetch` to be it's own hook it now cancels pending requests making sure we don't leak memory + it makes for an nicer looking API --- package.json | 1 + src/client-scopes/add/RoleMappingForm.tsx | 91 +++++----- src/client-scopes/details/MappingDetails.tsx | 95 +++++----- src/client-scopes/form/ClientScopeForm.tsx | 33 ++-- src/clients/ClientDetails.tsx | 41 +++-- src/clients/ClientsSection.tsx | 6 +- .../advanced/AuthenticationOverrides.tsx | 49 ++--- src/clients/credentials/Credentials.tsx | 56 +++--- src/clients/scopes/EvaluateScopes.tsx | 171 ++++++++---------- src/components/data-loader/DataLoader.tsx | 18 +- .../download-dialog/DownloadDialog.tsx | 41 ++--- .../role-mapping/AddRoleMappingModal.tsx | 83 ++++----- src/components/scroll-form/ScrollForm.tsx | 14 +- .../table-toolbar/KeycloakDataTable.tsx | 42 ++--- src/context/auth/AdminClient.tsx | 53 +++--- src/context/realm-context/RealmContext.tsx | 16 +- src/context/whoami/WhoAmI.tsx | 24 +-- src/groups/GroupsSection.tsx | 36 ++-- src/groups/MoveGroupDialog.tsx | 36 ++-- .../IdentityProvidersSection.tsx | 22 +-- src/realm-roles/AssociatedRolesModal.tsx | 28 +-- src/realm-settings/RealmSettingsSection.tsx | 24 +-- src/user-federation/UserFederationSection.tsx | 34 ++-- src/user/JoinGroupDialog.tsx | 69 ++++--- src/user/UserForm.tsx | 26 ++- src/user/UserGroups.tsx | 22 +-- src/user/UsersSection.tsx | 44 ++--- yarn.lock | 2 +- 28 files changed, 523 insertions(+), 654 deletions(-) diff --git a/package.json b/package.json index 88d2bd9c84..598eddd40a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@patternfly/react-core": "4.115.1", "@patternfly/react-icons": "4.10.1", "@patternfly/react-table": "4.26.7", + "axios": "^0.21.1", "file-saver": "^2.0.5", "i18next": "^19.6.2", "keycloak-admin": "1.14.16", diff --git a/src/client-scopes/add/RoleMappingForm.tsx b/src/client-scopes/add/RoleMappingForm.tsx index e98be38c1c..3f5f1a7c3c 100644 --- a/src/client-scopes/add/RoleMappingForm.tsx +++ b/src/client-scopes/add/RoleMappingForm.tsx @@ -1,8 +1,7 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, 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, @@ -24,10 +23,7 @@ import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation"; import { useAlerts } from "../../components/alert/Alerts"; import { RealmContext } from "../../context/realm-context/RealmContext"; -import { - useAdminClient, - asyncStateFetch, -} from "../../context/auth/AdminClient"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { ViewHeader } from "../../components/view-header/ViewHeader"; import { HelpItem } from "../../components/help-enabler/HelpItem"; @@ -36,7 +32,6 @@ 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(); @@ -51,53 +46,49 @@ export const RoleMappingForm = () => { const [selectedClient, setSelectedClient] = useState(); const [clientRoles, setClientRoles] = useState([]); - useEffect(() => { - return asyncStateFetch( - async () => { - const clients = await adminClient.clients.find(); + useFetch( + async () => { + const clients = await adminClient.clients.find(); - const asyncFilter = async ( - predicate: (client: ClientRepresentation) => Promise - ) => { - const results = await Promise.all(clients.map(predicate)); - return clients.filter((_, index) => results[index]); - }; + const asyncFilter = async ( + predicate: (client: ClientRepresentation) => Promise + ) => { + const results = await Promise.all(clients.map(predicate)); + return clients.filter((_, index) => results[index]); + }; - const filteredClients = await asyncFilter( - async (client) => - (await adminClient.clients.listRoles({ id: client.id! })).length > 0 - ); + const filteredClients = await asyncFilter( + async (client) => + (await adminClient.clients.listRoles({ id: client.id! })).length > 0 + ); - filteredClients.map( - (client) => - (client.toString = function () { - return this.clientId!; - }) - ); - return filteredClients; - }, - (filteredClients) => setClients(filteredClients), - handleError - ); - }, []); + filteredClients.map( + (client) => + (client.toString = function () { + return this.clientId!; + }) + ); + return filteredClients; + }, + (filteredClients) => setClients(filteredClients), + [] + ); - useEffect(() => { - return asyncStateFetch( - async () => { - const client = selectedClient as ClientRepresentation; - if (client && client.name !== "realmRoles") { - const clientRoles = await adminClient.clients.listRoles({ - id: client.id!, - }); - return clientRoles; - } else { - return await adminClient.roles.find(); - } - }, - (clientRoles) => setClientRoles(clientRoles), - handleError - ); - }, [selectedClient]); + useFetch( + async () => { + const client = selectedClient as ClientRepresentation; + if (client && client.name !== "realmRoles") { + const clientRoles = await adminClient.clients.listRoles({ + id: client.id!, + }); + return clientRoles; + } else { + return await adminClient.roles.find(); + } + }, + (clientRoles) => setClientRoles(clientRoles), + [selectedClient] + ); const save = async (mapping: ProtocolMapperRepresentation) => { try { diff --git a/src/client-scopes/details/MappingDetails.tsx b/src/client-scopes/details/MappingDetails.tsx index 0a482969a7..88671c53be 100644 --- a/src/client-scopes/details/MappingDetails.tsx +++ b/src/client-scopes/details/MappingDetails.tsx @@ -1,7 +1,6 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useErrorHandler } from "react-error-boundary"; import { ActionGroup, AlertVariant, @@ -24,10 +23,7 @@ import { ConfigPropertyRepresentation } from "keycloak-admin/lib/defs/configProp import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation"; import { ViewHeader } from "../../components/view-header/ViewHeader"; -import { - useAdminClient, - asyncStateFetch, -} from "../../context/auth/AdminClient"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { Controller, useForm } from "react-hook-form"; import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; import { useAlerts } from "../../components/alert/Alerts"; @@ -45,7 +41,6 @@ type Params = { export const MappingDetails = () => { const { t } = useTranslation("client-scopes"); const adminClient = useAdminClient(); - const handleError = useErrorHandler(); const { addAlert } = useAlerts(); const { id, mapperId } = useParams(); @@ -61,52 +56,50 @@ export const MappingDetails = () => { const serverInfo = useServerInfo(); const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/; - useEffect(() => { - return asyncStateFetch( - async () => { - if (mapperId.match(isGuid)) { - const data = await adminClient.clientScopes.findProtocolMapper({ - id, - mapperId, + useFetch( + async () => { + if (mapperId.match(isGuid)) { + const data = await adminClient.clientScopes.findProtocolMapper({ + id, + mapperId, + }); + if (data) { + Object.entries(data).map((entry) => { + convertToFormValues(entry[1], "config", setValue); }); - if (data) { - Object.entries(data).map((entry) => { - convertToFormValues(entry[1], "config", setValue); - }); - } - const mapperTypes = serverInfo.protocolMapperTypes![data!.protocol!]; - const properties = mapperTypes.find( - (type) => type.id === data!.protocolMapper - )?.properties!; - - return { - configProperties: properties, - mapping: data, - }; - } else { - const scope = await adminClient.clientScopes.findOne({ id }); - const protocolMappers = serverInfo.protocolMapperTypes![ - scope.protocol! - ]; - const mapping = protocolMappers.find( - (mapper) => mapper.id === mapperId - )!; - return { - mapping: { - name: mapping.name, - protocol: scope.protocol, - protocolMapper: mapperId, - }, - }; } - }, - (result) => { - setConfigProperties(result.configProperties); - setMapping(result.mapping); - }, - handleError - ); - }, []); + const mapperTypes = serverInfo.protocolMapperTypes![data!.protocol!]; + const properties = mapperTypes.find( + (type) => type.id === data!.protocolMapper + )?.properties!; + + return { + configProperties: properties, + mapping: data, + }; + } else { + const scope = await adminClient.clientScopes.findOne({ id }); + const protocolMappers = serverInfo.protocolMapperTypes![ + scope.protocol! + ]; + const mapping = protocolMappers.find( + (mapper) => mapper.id === mapperId + )!; + return { + mapping: { + name: mapping.name, + protocol: scope.protocol, + protocolMapper: mapperId, + }, + }; + } + }, + (result) => { + setConfigProperties(result.configProperties); + setMapping(result.mapping); + }, + [] + ); const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: "common:deleteMappingTitle", diff --git a/src/client-scopes/form/ClientScopeForm.tsx b/src/client-scopes/form/ClientScopeForm.tsx index f09bc65bac..b985e6ceec 100644 --- a/src/client-scopes/form/ClientScopeForm.tsx +++ b/src/client-scopes/form/ClientScopeForm.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; -import { useErrorHandler } from "react-error-boundary"; import { useTranslation } from "react-i18next"; import { AlertVariant, @@ -11,10 +10,7 @@ import { } from "@patternfly/react-core"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; -import { - useAdminClient, - asyncStateFetch, -} from "../../context/auth/AdminClient"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { KeycloakTabs } from "../../components/keycloak-tabs/KeycloakTabs"; import { useAlerts } from "../../components/alert/Alerts"; import { ViewHeader } from "../../components/view-header/ViewHeader"; @@ -30,7 +26,6 @@ export const ClientScopeForm = () => { const [hide, setHide] = useState(false); const adminClient = useAdminClient(); - const handleError = useErrorHandler(); const { id } = useParams<{ id: string }>(); const { addAlert } = useAlerts(); @@ -38,19 +33,17 @@ export const ClientScopeForm = () => { const [key, setKey] = useState(0); const refresh = () => setKey(new Date().getTime()); - useEffect(() => { - return asyncStateFetch( - async () => { - if (id) { - return await adminClient.clientScopes.findOne({ id }); - } - }, - (clientScope) => { - setClientScope(clientScope); - }, - handleError - ); - }, [key, id]); + useFetch( + async () => { + if (id) { + return await adminClient.clientScopes.findOne({ id }); + } + }, + (clientScope) => { + setClientScope(clientScope); + }, + [key, id] + ); const loader = async () => { const assignedRoles = hide diff --git a/src/clients/ClientDetails.tsx b/src/clients/ClientDetails.tsx index a894a84476..256974dd16 100644 --- a/src/clients/ClientDetails.tsx +++ b/src/clients/ClientDetails.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Alert, AlertVariant, @@ -12,9 +12,8 @@ import { TabTitleText, } from "@patternfly/react-core"; import { useHistory, useParams } from "react-router-dom"; -import { useErrorHandler } from "react-error-boundary"; import { useTranslation } from "react-i18next"; -import { Controller, FormProvider, useForm } from "react-hook-form"; +import { Controller, FormProvider, useForm, useWatch } from "react-hook-form"; import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import _ from "lodash"; @@ -26,7 +25,7 @@ import { } from "../components/confirm-dialog/ConfirmDialog"; import { DownloadDialog } from "../components/download-dialog/DownloadDialog"; import { ViewHeader } from "../components/view-header/ViewHeader"; -import { useAdminClient, asyncStateFetch } from "../context/auth/AdminClient"; +import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { Credentials } from "./credentials/Credentials"; import { convertFormValuesToObject, @@ -125,7 +124,6 @@ export const ClientDetails = () => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); const { addAlert } = useAlerts(); - const handleError = useErrorHandler(); const { realm } = useRealm(); const history = useHistory(); @@ -140,6 +138,12 @@ export const ClientDetails = () => { const form = useForm(); const { clientId } = useParams<{ clientId: string }>(); + const clientAuthenticatorType = useWatch({ + control: form.control, + name: "clientAuthenticatorType", + defaultValue: "client-secret", + }); + const [client, setClient] = useState(); const loader = async () => { @@ -178,16 +182,14 @@ export const ClientDetails = () => { }); }; - useEffect(() => { - return asyncStateFetch( - () => adminClient.clients.findOne({ id: clientId }), - (fetchedClient) => { - setClient(fetchedClient); - setupForm(fetchedClient); - }, - handleError - ); - }, [clientId]); + useFetch( + () => adminClient.clients.findOne({ id: clientId }), + (fetchedClient) => { + setClient(fetchedClient); + setupForm(fetchedClient); + }, + [clientId] + ); const save = async ( { confirmed = false, messageKey = "clientSaveSuccess" }: SaveOptions = { @@ -198,8 +200,7 @@ export const ClientDetails = () => { if (await form.trigger()) { if ( !client?.publicClient && - client?.clientAuthenticatorType !== - form.getValues("clientAuthenticatorType") && + client?.clientAuthenticatorType !== clientAuthenticatorType && !confirmed ) { toggleChangeAuthenticator(); @@ -241,7 +242,7 @@ export const ClientDetails = () => { { > <> {t("changeAuthenticatorConfirm", { - clientAuthenticatorType: form.getValues("clientAuthenticatorType"), + clientAuthenticatorType: clientAuthenticatorType, })} - {form.getValues("clientAuthenticatorType") === "client-jwt" && ( + {clientAuthenticatorType === "client-jwt" && ( )} diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index 214724c881..84e84e729d 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Link, useHistory } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { @@ -49,8 +49,6 @@ export const ClientsSection = () => { return await adminClient.clients.find({ ...params }); }; - useEffect(refresh, [selectedClient]); - const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("clientDelete", { clientId: selectedClient?.clientId }), messageKey: "clients:clientDeleteConfirm", @@ -62,7 +60,7 @@ export const ClientsSection = () => { id: selectedClient!.id!, }); addAlert(t("clientDeletedSuccess"), AlertVariant.success); - setSelectedClient(undefined); + refresh(); } catch (error) { addAlert(t("clientDeleteError", { error }), AlertVariant.danger); } diff --git a/src/clients/advanced/AuthenticationOverrides.tsx b/src/clients/advanced/AuthenticationOverrides.tsx index b546198f83..98d0007404 100644 --- a/src/clients/advanced/AuthenticationOverrides.tsx +++ b/src/clients/advanced/AuthenticationOverrides.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Control, Controller } from "react-hook-form"; import { useTranslation } from "react-i18next"; import _ from "lodash"; @@ -11,12 +11,8 @@ import { import { FormAccess } from "../../components/form-access/FormAccess"; import { HelpItem } from "../../components/help-enabler/HelpItem"; -import { - asyncStateFetch, - useAdminClient, -} from "../../context/auth/AdminClient"; +import { useFetch, useAdminClient } from "../../context/auth/AdminClient"; import { SaveReset } from "./SaveReset"; -import { useErrorHandler } from "react-error-boundary"; type AuthenticationOverridesProps = { control: Control>; @@ -34,32 +30,27 @@ export const AuthenticationOverrides = ({ const adminClient = useAdminClient(); const { t } = useTranslation("clients"); const [flows, setFlows] = useState([]); - const handleError = useErrorHandler(); const [browserFlowOpen, setBrowserFlowOpen] = useState(false); const [directGrantOpen, setDirectGrantOpen] = useState(false); - useEffect( - () => - asyncStateFetch( - () => adminClient.authenticationManagement.getFlows(), - (flows) => { - let filteredFlows = [ - ...flows.filter((flow) => flow.providerId !== "client-flow"), - ]; - filteredFlows = _.sortBy(filteredFlows, [(f) => f.alias]); - setFlows([ - - {t("common:choose")} - , - ...filteredFlows.map((flow) => ( - - {flow.alias} - - )), - ]); - }, - handleError - ), + useFetch( + () => adminClient.authenticationManagement.getFlows(), + (flows) => { + let filteredFlows = [ + ...flows.filter((flow) => flow.providerId !== "client-flow"), + ]; + filteredFlows = _.sortBy(filteredFlows, [(f) => f.alias]); + setFlows([ + + {t("common:choose")} + , + ...filteredFlows.map((flow) => ( + + {flow.alias} + + )), + ]); + }, [] ); diff --git a/src/clients/credentials/Credentials.tsx b/src/clients/credentials/Credentials.tsx index d53331d0fa..30ab5759c8 100644 --- a/src/clients/credentials/Credentials.tsx +++ b/src/clients/credentials/Credentials.tsx @@ -24,10 +24,7 @@ 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 { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { ClientSecret } from "./ClientSecret"; import { SignedJWT } from "./SignedJWT"; @@ -55,8 +52,12 @@ export type CredentialsProps = { export const Credentials = ({ clientId, save }: CredentialsProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); - const handleError = useErrorHandler(); const { addAlert } = useAlerts(); + + const [providers, setProviders] = useState( + [] + ); + const { control, formState: { isDirty }, @@ -64,37 +65,33 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => { const clientAuthenticatorType = useWatch({ control: control, name: "clientAuthenticatorType", + defaultValue: "", }); - const [providers, setProviders] = useState( - [] - ); const [secret, setSecret] = useState(""); const [accessToken, setAccessToken] = useState(""); const [open, isOpen] = useState(false); - useEffect(() => { - return asyncStateFetch( - async () => { - const providers = await adminClient.authenticationManagement.getClientAuthenticatorProviders( - { id: clientId } - ); + useFetch( + async () => { + const providers = await adminClient.authenticationManagement.getClientAuthenticatorProviders( + { id: clientId } + ); - const secret = await adminClient.clients.getClientSecret({ - id: clientId, - }); - return { - providers, - secret: secret.value!, - }; - }, - ({ providers, secret }) => { - setProviders(providers); - setSecret(secret); - }, - handleError - ); - }, []); + const secret = await adminClient.clients.getClientSecret({ + id: clientId, + }); + return { + providers, + secret: secret.value!, + }; + }, + ({ providers, secret }) => { + setProviders(providers); + setSecret(secret); + }, + [] + ); async function regenerate( call: (clientId: string) => Promise, @@ -164,6 +161,7 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => { (