From 80cca9eca48e89b6368d4fd801291a1926b6c9a7 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Sun, 14 Feb 2021 19:29:43 +0100 Subject: [PATCH] Limit search users when users federated or large size (#345) * when external users then users need to search * added email not verified icon + delete confirm * handle search errors * removed text * fixed label counter --- .../table-toolbar/PaginatingTableToolbar.tsx | 2 +- src/user/SearchUser.tsx | 1 - src/user/UsersSection.tsx | 250 ++++++++++++------ src/user/messages.json | 12 +- src/user/user-section.css | 4 + 5 files changed, 189 insertions(+), 80 deletions(-) create mode 100644 src/user/user-section.css diff --git a/src/components/table-toolbar/PaginatingTableToolbar.tsx b/src/components/table-toolbar/PaginatingTableToolbar.tsx index 5262e6cc40..510affb015 100644 --- a/src/components/table-toolbar/PaginatingTableToolbar.tsx +++ b/src/components/table-toolbar/PaginatingTableToolbar.tsx @@ -44,7 +44,7 @@ export const PaginatingTableToolbar = ({ isCompact toggleTemplate={({ firstIndex, lastIndex }: ToggleTemplateProps) => ( - {firstIndex} - {lastIndex} + {firstIndex} - {lastIndex! - (count < max ? 1 : 0)} )} itemCount={count + page * max + (count <= max ? 1 : 0)} diff --git a/src/user/SearchUser.tsx b/src/user/SearchUser.tsx index be7fff3b3a..b8a15d9a1f 100644 --- a/src/user/SearchUser.tsx +++ b/src/user/SearchUser.tsx @@ -26,7 +26,6 @@ export const SearchUser = ({ onSearch }: SearchUserProps) => { {t("startBySearchingAUser")} - {t("startIntro")}
onSearch(form.search))}> ; @@ -26,48 +37,89 @@ export const UsersSection = () => { const adminClient = useAdminClient(); const { addAlert } = useAlerts(); const { realm: realmName } = useContext(RealmContext); + const [listUsers, setListUsers] = useState(false); + const [initialSearch, setInitialSearch] = useState(""); + const [selectedRows, setSelectedRows] = useState([]); const [key, setKey] = useState(""); const refresh = () => setKey(`${new Date().getTime()}`); + useEffect(() => { + return asyncStateFetch( + () => { + const testParams = { + type: "org.keycloak.storage.UserStorageProvider", + }; + + return Promise.all([ + adminClient.components.find(testParams), + adminClient.users.count(), + ]); + }, + (response) => { + //should *only* list users when no user federation is configured and uses count > 100 + setListUsers( + !((response[0] && response[0].length > 0) || response[1] > 100) + ); + } + ); + }, []); + const loader = async (first?: number, max?: number, search?: string) => { const params: { [name: string]: string | number } = { first: first!, max: max!, }; - if (search) { - params.search = search; + const searchParam = search || initialSearch || ""; + if (searchParam) { + params.search = searchParam; } - const users = await adminClient.users.find({ ...params }); - const realm = await adminClient.realms.findOne({ realm: realmName }); - if (realm?.bruteForceProtected) { - const brutes = await Promise.all( - users.map((user: BruteUser) => - adminClient.attackDetection.findOne({ - id: user.id!, - }) - ) - ); - for (let index = 0; index < users.length; index++) { - const user: BruteUser = users[index]; - user.brute = brutes[index]; - } + if (!listUsers && !searchParam) { + return []; } - - return users; - }; - - const deleteUser = async (user: UserRepresentation) => { try { - await adminClient.users.del({ id: user.id! }); - refresh(); - addAlert(t("userDeletedSuccess"), AlertVariant.success); + const users = await adminClient.users.find({ ...params }); + const realm = await adminClient.realms.findOne({ realm: realmName }); + if (realm?.bruteForceProtected) { + const brutes = await Promise.all( + users.map((user: BruteUser) => + adminClient.attackDetection.findOne({ + id: user.id!, + }) + ) + ); + for (let index = 0; index < users.length; index++) { + const user: BruteUser = users[index]; + user.brute = brutes[index]; + } + } + return users; } catch (error) { - addAlert(t("userDeletedError", { error }), AlertVariant.danger); + addAlert(t("noUsersFoundError", { error }), AlertVariant.danger); + return []; } }; + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "users:deleteConfirm", + messageKey: t("deleteConfirmDialog", { count: selectedRows.length }), + continueButtonLabel: "delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + for (const user of selectedRows) { + await adminClient.users.del({ id: user.id! }); + } + setSelectedRows([]); + refresh(); + addAlert(t("userDeletedSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("userDeletedError", { error }), AlertVariant.danger); + } + }, + }); + const StatusRow = (user: BruteUser) => { return ( <> @@ -81,62 +133,108 @@ export const UsersSection = () => { {t("temporaryDisabled")} )} + {user.enabled && !user.brute?.disabled && "—"} + + ); + }; + + const ValidatedEmail = (user: UserRepresentation) => { + return ( + <> + {!user.emailVerified && ( + {t("notVerified")}} + > + + + )}{" "} + {emptyFormatter()(user.email)} ); }; return ( <> - + + - - - - - - - - - } - actions={[ - { - title: t("common:delete"), - onRowClick: (user) => { - deleteUser(user); + {!listUsers && !initialSearch && ( + { + setInitialSearch(search); + }} + /> + )} + {(listUsers || initialSearch) && ( + setSelectedRows([...rows])} + emptyState={ + {}} + /> + } + toolbarItem={ + <> + + + + + + + + } + actions={[ + { + title: t("common:delete"), + onRowClick: (user) => { + setSelectedRows([user]); + toggleDeleteDialog(); + }, }, - }, - ]} - columns={[ - { - name: "username", - displayKey: "users:username", - }, - { - name: "email", - displayKey: "users:email", - }, - { - name: "lastName", - displayKey: "users:lastName", - }, - { - name: "firstName", - displayKey: "users:firstName", - }, - { - name: "status", - displayKey: "users:status", - cellRenderer: StatusRow, - }, - ]} - /> + ]} + columns={[ + { + name: "username", + displayKey: "users:username", + }, + { + name: "email", + displayKey: "users:email", + cellRenderer: ValidatedEmail, + }, + { + name: "lastName", + displayKey: "users:lastName", + cellFormatters: [emptyFormatter()], + }, + { + name: "firstName", + displayKey: "users:firstName", + cellFormatters: [emptyFormatter()], + }, + { + name: "status", + displayKey: "users:status", + cellRenderer: StatusRow, + }, + ]} + /> + )} ); diff --git a/src/user/messages.json b/src/user/messages.json index 86bdc12293..3f434c0e69 100644 --- a/src/user/messages.json +++ b/src/user/messages.json @@ -1,8 +1,12 @@ { "users": { "title": "Users", - "userExplain": "This is the description", "searchForUser": "Search user", + "startBySearchingAUser": "Start by searching a user", + "createNewUser": "Create new user", + "noUsersFound": "No users found", + "noUsersFoundError": "No users found due to {{error}}", + "emptyInstructions": "Change your search criteria or add a user", "username": "Username", "email": "Email", "lastName": "Last name", @@ -10,9 +14,13 @@ "status": "Status", "disabled": "Disabled", "temporaryDisabled": "Temporarily disabled", + "notVerified": "Not verified", "addUser": "Add user", "deleteUser": "Delete user", + "deleteConfirm": "Delete user?", + "deleteConfirmDialog": "Are you sure you want to permanently delete {{count}} selected user", + "deleteConfirmDialog_plural": "Are you sure you want to permanently delete {{count}} selected users", "userDeletedSuccess": "The user has been deleted", - "userDeletedError": "The user could not be deleted {error}" + "userDeletedError": "The user could not be deleted {{error}}" } } diff --git a/src/user/user-section.css b/src/user/user-section.css new file mode 100644 index 0000000000..2ae42e62c0 --- /dev/null +++ b/src/user/user-section.css @@ -0,0 +1,4 @@ + +.keycloak__user-section__email-verified { + color: var(--pf-global--danger-color--100); +} \ No newline at end of file