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
This commit is contained in:
parent
afaa08fbc6
commit
80cca9eca4
5 changed files with 189 additions and 80 deletions
|
@ -44,7 +44,7 @@ export const PaginatingTableToolbar = ({
|
|||
isCompact
|
||||
toggleTemplate={({ firstIndex, lastIndex }: ToggleTemplateProps) => (
|
||||
<b>
|
||||
{firstIndex} - {lastIndex}
|
||||
{firstIndex} - {lastIndex! - (count < max ? 1 : 0)}
|
||||
</b>
|
||||
)}
|
||||
itemCount={count + page * max + (count <= max ? 1 : 0)}
|
||||
|
|
|
@ -26,7 +26,6 @@ export const SearchUser = ({ onSearch }: SearchUserProps) => {
|
|||
{t("startBySearchingAUser")}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{t("startIntro")}
|
||||
<Form onSubmit={handleSubmit((form) => onSearch(form.search))}>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertVariant,
|
||||
|
@ -7,15 +7,26 @@ import {
|
|||
Label,
|
||||
PageSection,
|
||||
ToolbarItem,
|
||||
Tooltip,
|
||||
} from "@patternfly/react-core";
|
||||
import { InfoCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons";
|
||||
import {
|
||||
ExclamationCircleIcon,
|
||||
InfoCircleIcon,
|
||||
WarningTriangleIcon,
|
||||
} from "@patternfly/react-icons";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||
import { SearchUser } from "./SearchUser";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { emptyFormatter } from "../util";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
|
||||
import "./user-section.css";
|
||||
|
||||
type BruteUser = UserRepresentation & {
|
||||
brute?: Record<string, object>;
|
||||
|
@ -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<UserRepresentation[]>([]);
|
||||
|
||||
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")}
|
||||
</Label>
|
||||
)}
|
||||
{user.enabled && !user.brute?.disabled && "—"}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ValidatedEmail = (user: UserRepresentation) => {
|
||||
return (
|
||||
<>
|
||||
{!user.emailVerified && (
|
||||
<Tooltip
|
||||
key={`email-verified-${user.id}`}
|
||||
content={<>{t("notVerified")}</>}
|
||||
>
|
||||
<ExclamationCircleIcon className="keycloak__user-section__email-verified" />
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
{emptyFormatter()(user.email)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey="users:title" subKey="users:userExplain" />
|
||||
<DeleteConfirm />
|
||||
<ViewHeader titleKey="users:title" subKey="" />
|
||||
<PageSection variant="light">
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="users:title"
|
||||
searchPlaceholderKey="users:searchForUser"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button>{t("addUser")}</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button variant={ButtonVariant.plain}>{t("deleteUser")}</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (user) => {
|
||||
deleteUser(user);
|
||||
{!listUsers && !initialSearch && (
|
||||
<SearchUser
|
||||
onSearch={(search) => {
|
||||
setInitialSearch(search);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(listUsers || initialSearch) && (
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="users:title"
|
||||
searchPlaceholderKey="users:searchForUser"
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("noUsersFound")}
|
||||
instructions={t("emptyInstructions")}
|
||||
primaryActionText={t("createNewUser")}
|
||||
onPrimaryAction={() => {}}
|
||||
/>
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button>{t("addUser")}</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant={ButtonVariant.plain}
|
||||
onClick={toggleDeleteDialog}
|
||||
>
|
||||
{t("deleteUser")}
|
||||
</Button>
|
||||
</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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}}"
|
||||
}
|
||||
}
|
||||
|
|
4
src/user/user-section.css
Normal file
4
src/user/user-section.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
.keycloak__user-section__email-verified {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
Loading…
Reference in a new issue