Added user permission tab (#2501)
This commit is contained in:
parent
97aaf98e49
commit
eb3d3714fc
13 changed files with 434 additions and 269 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -7,7 +7,7 @@
|
|||
"name": "keycloak-admin-ui",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^18.0.0-dev.22",
|
||||
"@keycloak/keycloak-admin-client": "^18.0.1-dev.1",
|
||||
"@patternfly/patternfly": "^4.185.1",
|
||||
"@patternfly/react-code-editor": "^4.43.16",
|
||||
"@patternfly/react-core": "^4.202.16",
|
||||
|
@ -3941,9 +3941,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@keycloak/keycloak-admin-client": {
|
||||
"version": "18.0.0-dev.22",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.0-dev.22.tgz",
|
||||
"integrity": "sha512-xWiZip5uzVoYkzJA3uJWqIKWy1krd9b9XgNzdhibw1uLbo9HGTzpdn8ViAXQrpgFxx5tgws0cTQrSduHHZsbFg==",
|
||||
"version": "18.0.1-dev.1",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.1-dev.1.tgz",
|
||||
"integrity": "sha512-GYNtOkyiP2Xq18Hb6o3lGE/KQIBD2B4U3I6APU+18wwCnjN1pDEffZi1LN6U4BHjelH4HJlpFPEFFKdbT4el+A==",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
@ -26674,9 +26674,9 @@
|
|||
}
|
||||
},
|
||||
"@keycloak/keycloak-admin-client": {
|
||||
"version": "18.0.0-dev.22",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.0-dev.22.tgz",
|
||||
"integrity": "sha512-xWiZip5uzVoYkzJA3uJWqIKWy1krd9b9XgNzdhibw1uLbo9HGTzpdn8ViAXQrpgFxx5tgws0cTQrSduHHZsbFg==",
|
||||
"version": "18.0.1-dev.1",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.1-dev.1.tgz",
|
||||
"integrity": "sha512-GYNtOkyiP2Xq18Hb6o3lGE/KQIBD2B4U3I6APU+18wwCnjN1pDEffZi1LN6U4BHjelH4HJlpFPEFFKdbT4el+A==",
|
||||
"requires": {
|
||||
"axios": "^0.26.1",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"server:import-client": "./scripts/import-client.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^18.0.0-dev.22",
|
||||
"@keycloak/keycloak-admin-client": "^18.0.1-dev.1",
|
||||
"@patternfly/patternfly": "^4.185.1",
|
||||
"@patternfly/react-code-editor": "^4.43.16",
|
||||
"@patternfly/react-core": "^4.202.16",
|
||||
|
|
|
@ -69,7 +69,7 @@ import {
|
|||
import { toClientScopesTab } from "./routes/ClientScopeTab";
|
||||
import { AuthorizationExport } from "./authorization/AuthorizationExport";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { PermissionsTab } from "./permissions/PermissionTab";
|
||||
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
||||
import { keyValueToArray } from "../components/key-value-form/key-value-convert";
|
||||
|
||||
type ClientDetailHeaderProps = {
|
||||
|
@ -524,7 +524,9 @@ export default function ClientDetails() {
|
|||
<Tab
|
||||
id="permissions"
|
||||
data-testid="authorizationPermissions"
|
||||
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
||||
title={
|
||||
<TabTitleText>{t("common:permissions")}</TabTitleText>
|
||||
}
|
||||
{...authenticationRoute("permissions")}
|
||||
>
|
||||
<AuthorizationPermissions clientId={clientId} />
|
||||
|
@ -565,10 +567,10 @@ export default function ClientDetails() {
|
|||
<Tab
|
||||
id="permissions"
|
||||
data-testid="permissionsTab"
|
||||
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
||||
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
|
||||
{...route("permissions")}
|
||||
>
|
||||
<PermissionsTab clientId={client.id!} />
|
||||
<PermissionsTab id={client.id!} type="clients" />
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
|
|
|
@ -167,7 +167,6 @@ export default {
|
|||
resourceDeletedSuccess: "The resource successfully deleted",
|
||||
resourceDeletedError: "Could not remove the resource {{error}}",
|
||||
identityInformation: "Identity Information",
|
||||
permissions: "Permissions",
|
||||
searchForPermission: "Search for permission",
|
||||
deleteScope: "Permanently delete authorization scope?",
|
||||
deleteScopeConfirm:
|
||||
|
@ -554,23 +553,5 @@ export default {
|
|||
never: "Never expires",
|
||||
},
|
||||
mappers: "Mappers",
|
||||
|
||||
permissionsEnabled: "Permissions enabled",
|
||||
scopePermissions: {
|
||||
"manage-description":
|
||||
"Policies that decide if an administrator can manage this client",
|
||||
"configure-description":
|
||||
"Reduced management permissions for administrator. Cannot set scope, template, or protocol mappers.",
|
||||
"view-description":
|
||||
"Policies that decide if an administrator can view this client",
|
||||
"map-roles-description":
|
||||
"Policies that decide if an administrator can map roles defined by this client",
|
||||
"map-roles-client-scope-description":
|
||||
"Policies that decide if an administrator can apply roles defined by this client to the client scope of another client",
|
||||
"map-roles-composite-description":
|
||||
"Policies that decide if an administrator can apply roles defined by this client as a composite to another role",
|
||||
"token-exchange-description":
|
||||
"Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, FormGroup, PageSection, Switch } from "@patternfly/react-core";
|
||||
import type { IRowData } from "@patternfly/react-table";
|
||||
|
||||
import type { ManagementPermissionReference } from "@keycloak/keycloak-admin-client/lib/defs/managementPermissionReference";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { toPermissionDetails } from "../routes/PermissionDetails";
|
||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
import "./permissions-tab.css";
|
||||
|
||||
type PermissionsTabProps = {
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
export const PermissionsTab = ({ clientId }: PermissionsTabProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const history = useHistory();
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const [realmId, setRealmId] = useState("");
|
||||
const [permission, setPermission] = useState<ManagementPermissionReference>();
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
adminClient.clients.find({
|
||||
search: true,
|
||||
clientId: realm,
|
||||
}),
|
||||
adminClient.clients.listFineGrainPermissions({ id: clientId }),
|
||||
]).then(([clients, permission]) => {
|
||||
setRealmId(clients[0]?.id!);
|
||||
setPermission(permission);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const PermissionDetailLink = (permission: Record<string, string>) => (
|
||||
<Link
|
||||
key={permission.id}
|
||||
to={toPermissionDetails({
|
||||
realm,
|
||||
id: realmId,
|
||||
permissionType: "scope",
|
||||
permissionId: permission.id,
|
||||
})}
|
||||
>
|
||||
{permission.name}
|
||||
</Link>
|
||||
);
|
||||
|
||||
if (!permission) {
|
||||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
<PageSection variant="light">
|
||||
<Form isHorizontal>
|
||||
<FormGroup
|
||||
className="permission-label"
|
||||
label={t("permissionsEnabled")}
|
||||
fieldId="permissionsEnabled"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:permissionsEnabled"
|
||||
fieldLabelId="clients:permissionsEnabled"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="permissionsEnabled"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={permission.enabled}
|
||||
onChange={async (enabled) => {
|
||||
const p = await adminClient.clients.updateFineGrainPermission(
|
||||
{ id: clientId },
|
||||
{ enabled }
|
||||
);
|
||||
setPermission(p);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</PageSection>
|
||||
<KeycloakDataTable
|
||||
loader={Object.entries(permission.scopePermissions || {}).map(
|
||||
([name, id]) => ({
|
||||
id,
|
||||
name,
|
||||
})
|
||||
)}
|
||||
ariaLabelKey="clients:permissions"
|
||||
searchPlaceholderKey="clients:searchForPermission"
|
||||
actionResolver={(rowData: IRowData) => {
|
||||
const permission: Record<string, string> = rowData.data;
|
||||
return [
|
||||
{
|
||||
title: t("common:edit"),
|
||||
onClick() {
|
||||
history.push(
|
||||
toPermissionDetails({
|
||||
realm,
|
||||
id: realmId,
|
||||
permissionType: "scope",
|
||||
permissionId: permission.id,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
name: "scopeName",
|
||||
displayKey: "common:name",
|
||||
cellRenderer: PermissionDetailLink,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
displayKey: "common:description",
|
||||
cellRenderer: (permission: Record<string, string>) =>
|
||||
t(`scopePermissions.${permission.name}-description`),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
.permission-label > .pf-c-form__group-label {
|
||||
width: 120%;
|
||||
}
|
|
@ -70,6 +70,7 @@ export default {
|
|||
retry: "Press here to refresh and continue",
|
||||
plus: "Plus",
|
||||
minus: "Minus",
|
||||
confirm: "Confirm",
|
||||
|
||||
clientScope: {
|
||||
default: "Default",
|
||||
|
@ -89,6 +90,52 @@ export default {
|
|||
sessions: "Sessions",
|
||||
events: "Events",
|
||||
mappers: "Mappers",
|
||||
permissions: "Permissions",
|
||||
permissionsList: "Permission list",
|
||||
permissionsListIntro:
|
||||
"Edit the permission list by clicking the scope-name. It then redirects to the permission details page of the client named <1>{{realm}}</1>",
|
||||
usersPermissionsHint:
|
||||
"Fine grained permissions for managing all users in realm. You can define different policies for who is allowed to manage users in the realm.",
|
||||
clientsPermissionsHint:
|
||||
"Fine grained permissions for administrators that want to manage this client or apply roles defined by this client.",
|
||||
|
||||
permissionsScopeName: "Scope-name",
|
||||
permissionsEnabled: "Permissions enabled",
|
||||
permissionsDisable: "Disable permissions?",
|
||||
permissionsDisableConfirm:
|
||||
"If you disable the permissions, all the permissions in the list below will be delete automatically. In addition, the resources and scopes that are related will be removed",
|
||||
scopePermissions: {
|
||||
clients: {
|
||||
"manage-description":
|
||||
"Policies that decide if an administrator can manage this client",
|
||||
"configure-description":
|
||||
"Reduced management permissions for administrator. Cannot set scope, template, or protocol mappers.",
|
||||
"view-description":
|
||||
"Policies that decide if an administrator can view this client",
|
||||
"map-roles-description":
|
||||
"Policies that decide if an administrator can map roles defined by this client",
|
||||
"map-roles-client-scope-description":
|
||||
"Policies that decide if an administrator can apply roles defined by this client to the client scope of another client",
|
||||
"map-roles-composite-description":
|
||||
"Policies that decide if an administrator can apply roles defined by this client as a composite to another role",
|
||||
"token-exchange-description":
|
||||
"Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.",
|
||||
},
|
||||
users: {
|
||||
"view-description":
|
||||
"Policies that decide if an administrator can view all users in realm",
|
||||
"manage-description":
|
||||
"Policies that decide if an administrator can manage all users in the realm",
|
||||
"map-roles-description":
|
||||
"Policies that decide if administrator can map roles for all users",
|
||||
"manage-group-membership-description":
|
||||
"Policies that decide if an administrator can manage group membership for all users in the realm. This is used in conjunction with specific group policy",
|
||||
"impersonate-description":
|
||||
"Policies that decide if administrator can impersonate other users",
|
||||
"user-impersonated-description":
|
||||
"Policies that decide which users can be impersonated. These policies are applied to the user being impersonated.",
|
||||
},
|
||||
},
|
||||
|
||||
configure: "Configure",
|
||||
realmSettings: "Realm settings",
|
||||
|
|
218
src/components/permission-tab/PermissionTab.tsx
Normal file
218
src/components/permission-tab/PermissionTab.tsx
Normal file
|
@ -0,0 +1,218 @@
|
|||
import React, { useState } from "react";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Switch,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
ActionsColumn,
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
|
||||
import type { ManagementPermissionReference } from "@keycloak/keycloak-admin-client/lib/defs/managementPermissionReference";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { toPermissionDetails } from "../../clients/routes/PermissionDetails";
|
||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
||||
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
|
||||
|
||||
import "./permissions-tab.css";
|
||||
|
||||
type PermissionScreenType = "clients" | "users";
|
||||
|
||||
type PermissionsTabProps = {
|
||||
id?: string;
|
||||
type: PermissionScreenType;
|
||||
};
|
||||
|
||||
export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const history = useHistory();
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
const [realmId, setRealmId] = useState("");
|
||||
const [permission, setPermission] = useState<ManagementPermissionReference>();
|
||||
|
||||
const togglePermissionEnabled = (enabled: boolean) => {
|
||||
switch (type) {
|
||||
case "clients":
|
||||
return adminClient.clients.updateFineGrainPermission(
|
||||
{ id: id! },
|
||||
{ enabled }
|
||||
);
|
||||
case "users":
|
||||
return adminClient.realms.updateUsersManagementPermissions({
|
||||
realm,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
Promise.all([
|
||||
adminClient.clients.find({
|
||||
search: true,
|
||||
clientId: realm,
|
||||
}),
|
||||
(() => {
|
||||
switch (type) {
|
||||
case "clients":
|
||||
return adminClient.clients.listFineGrainPermissions({ id: id! });
|
||||
case "users":
|
||||
return adminClient.realms.getUsersManagementPermissions({
|
||||
realm,
|
||||
});
|
||||
}
|
||||
})(),
|
||||
]),
|
||||
([clients, permission]) => {
|
||||
setRealmId(clients[0]?.id!);
|
||||
setPermission(permission);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "common:permissionsDisable",
|
||||
messageKey: "common:permissionsDisableConfirm",
|
||||
continueButtonLabel: "common:confirm",
|
||||
onConfirm: async () => {
|
||||
const permission = await togglePermissionEnabled(false);
|
||||
setPermission(permission);
|
||||
},
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<DisableConfirm />
|
||||
<Card isFlat>
|
||||
<CardTitle>{t("permissions")}</CardTitle>
|
||||
<CardBody>
|
||||
{t(`${type}PermissionsHint`)}
|
||||
<Form isHorizontal className="pf-u-pt-md">
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
className="permission-label"
|
||||
label={t("permissionsEnabled")}
|
||||
fieldId="permissionsEnabled"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:permissionsEnabled"
|
||||
fieldLabelId="clients:permissionsEnabled"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="permissionsEnabled"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={permission.enabled}
|
||||
onChange={async (enabled) => {
|
||||
if (enabled) {
|
||||
const permission = await togglePermissionEnabled(enabled);
|
||||
setPermission(permission);
|
||||
} else {
|
||||
toggleDisableDialog();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{permission.enabled && (
|
||||
<>
|
||||
<Card isFlat className="pf-u-mt-lg">
|
||||
<CardTitle>{t("permissionsList")}</CardTitle>
|
||||
<CardBody>
|
||||
<Trans i18nKey="common:permissionsListIntro">
|
||||
{" "}
|
||||
<strong>{{ realm: `${realm}-realm` }}</strong>.
|
||||
</Trans>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card isFlat className="keycloak__permission__permission-table">
|
||||
<CardBody className="pf-u-p-0">
|
||||
<TableComposable
|
||||
aria-label={t("permissionsList")}
|
||||
variant="compact"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th id="permissionsScopeName">
|
||||
{t("permissionsScopeName")}
|
||||
</Th>
|
||||
<Th id="description">{t("description")}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(permission.scopePermissions || {})
|
||||
.sort((a, b) =>
|
||||
a[0]!.localeCompare(b[0]!, whoAmI.getLocale())
|
||||
)
|
||||
.map(([name, id]) => (
|
||||
<Tr key={id}>
|
||||
<Td>
|
||||
<Link
|
||||
to={toPermissionDetails({
|
||||
realm,
|
||||
id: realmId,
|
||||
permissionType: "scope",
|
||||
permissionId: id,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</Td>
|
||||
<Td>
|
||||
{t(`scopePermissions.${type}.${name}-description`)}
|
||||
</Td>
|
||||
<Td isActionCell>
|
||||
<ActionsColumn
|
||||
items={[
|
||||
{
|
||||
title: t("common:edit"),
|
||||
onClick() {
|
||||
history.push(
|
||||
toPermissionDetails({
|
||||
realm,
|
||||
id: realmId,
|
||||
permissionType: "scope",
|
||||
permissionId: id,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</PageSection>
|
||||
);
|
||||
};
|
9
src/components/permission-tab/permissions-tab.css
Normal file
9
src/components/permission-tab/permissions-tab.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
.permission-label > .pf-c-form__group-label {
|
||||
width: 120%;
|
||||
}
|
||||
|
||||
.keycloak__permission__permission-table.pf-c-card {
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
|
@ -9,6 +9,8 @@ import {
|
|||
KebabToggle,
|
||||
Label,
|
||||
PageSection,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInput,
|
||||
|
@ -42,6 +44,12 @@ import { toUser } from "./routes/User";
|
|||
import { toAddUser } from "./routes/AddUser";
|
||||
import helpUrls from "../help-urls";
|
||||
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
||||
import { toUsers, UserTab } from "./routes/Users";
|
||||
import {
|
||||
routableTab,
|
||||
RoutableTabs,
|
||||
} from "../components/routable-tabs/RoutableTabs";
|
||||
|
||||
import "./user-section.css";
|
||||
|
||||
|
@ -277,6 +285,15 @@ export default function UsersSection() {
|
|||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
const route = (tab: UserTab) =>
|
||||
routableTab({
|
||||
to: toUsers({
|
||||
realm: realmName,
|
||||
tab,
|
||||
}),
|
||||
history,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
|
@ -285,111 +302,137 @@ export default function UsersSection() {
|
|||
titleKey="users:title"
|
||||
subKey="users:usersExplain"
|
||||
helpUrl={helpUrls.usersUrl}
|
||||
divider={false}
|
||||
/>
|
||||
<PageSection
|
||||
data-testid="users-page"
|
||||
variant="light"
|
||||
className="pf-u-p-0"
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="users:title"
|
||||
searchPlaceholderKey="users:searchForUser"
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
emptyState={
|
||||
!listUsers ? (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name="search-input"
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={t("users:searchForUser")}
|
||||
onChange={(value) => {
|
||||
setSearchUser(value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
refresh();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("common:search")}
|
||||
onClick={refresh}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
{toolbar}
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<EmptyState data-testid="empty-state" variant="large">
|
||||
<TextContent className="kc-search-users-text">
|
||||
<Text>{t("searchForUserDescription")}</Text>
|
||||
</TextContent>
|
||||
</EmptyState>
|
||||
</>
|
||||
) : (
|
||||
<ListEmptyState
|
||||
message={t("noUsersFound")}
|
||||
instructions={t("emptyInstructions")}
|
||||
primaryActionText={t("createNewUser")}
|
||||
onPrimaryAction={goToCreate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
toolbarItem={toolbar}
|
||||
actionResolver={(rowData: IRowData) => {
|
||||
const user: UserRepresentation = rowData.data;
|
||||
if (!user.access?.manage) return [];
|
||||
<RoutableTabs
|
||||
data-testid="user-tabs"
|
||||
defaultLocation={toUsers({
|
||||
realm: realmName,
|
||||
tab: "list",
|
||||
})}
|
||||
isBox
|
||||
mountOnEnter
|
||||
>
|
||||
<Tab
|
||||
id="list"
|
||||
data-testid="listTab"
|
||||
title={<TabTitleText>{t("userList")}</TabTitleText>}
|
||||
{...route("list")}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="users:title"
|
||||
searchPlaceholderKey="users:searchForUser"
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
emptyState={
|
||||
!listUsers ? (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name="search-input"
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={t("users:searchForUser")}
|
||||
onChange={(value) => {
|
||||
setSearchUser(value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
refresh();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("common:search")}
|
||||
onClick={refresh}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
{toolbar}
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<EmptyState data-testid="empty-state" variant="large">
|
||||
<TextContent className="kc-search-users-text">
|
||||
<Text>{t("searchForUserDescription")}</Text>
|
||||
</TextContent>
|
||||
</EmptyState>
|
||||
</>
|
||||
) : (
|
||||
<ListEmptyState
|
||||
message={t("noUsersFound")}
|
||||
instructions={t("emptyInstructions")}
|
||||
primaryActionText={t("createNewUser")}
|
||||
onPrimaryAction={goToCreate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
toolbarItem={toolbar}
|
||||
actionResolver={(rowData: IRowData) => {
|
||||
const user: UserRepresentation = rowData.data;
|
||||
if (!user.access?.manage) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onClick: () => {
|
||||
setSelectedRows([user]);
|
||||
toggleDeleteDialog();
|
||||
return [
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onClick: () => {
|
||||
setSelectedRows([user]);
|
||||
toggleDeleteDialog();
|
||||
},
|
||||
},
|
||||
];
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
displayKey: "users:username",
|
||||
cellRenderer: UserDetailLink,
|
||||
},
|
||||
},
|
||||
];
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
displayKey: "users:username",
|
||||
cellRenderer: UserDetailLink,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="permissions"
|
||||
data-testid="permissionsTab"
|
||||
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
|
||||
{...route("permissions")}
|
||||
>
|
||||
<PermissionsTab type="users" />
|
||||
</Tab>
|
||||
</RoutableTabs>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ export default {
|
|||
users: {
|
||||
title: "Users",
|
||||
usersExplain: "Placeholder for users explanation.",
|
||||
userList: "User list",
|
||||
searchForUser: "Search user",
|
||||
startBySearchingAUser: "Start by searching for users",
|
||||
searchForUserDescription:
|
||||
|
|
|
@ -3,6 +3,6 @@ import { AddUserRoute } from "./routes/AddUser";
|
|||
import { UserRoute } from "./routes/User";
|
||||
import { UsersRoute } from "./routes/Users";
|
||||
|
||||
const routes: RouteDef[] = [UsersRoute, AddUserRoute, UserRoute];
|
||||
const routes: RouteDef[] = [AddUserRoute, UsersRoute, UserRoute];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -3,10 +3,12 @@ import { lazy } from "react";
|
|||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
|
||||
export type UsersParams = { realm: string };
|
||||
export type UserTab = "list" | "permissions";
|
||||
|
||||
export type UsersParams = { realm: string; tab?: UserTab };
|
||||
|
||||
export const UsersRoute: RouteDef = {
|
||||
path: "/:realm/users",
|
||||
path: "/:realm/users/:tab?",
|
||||
component: lazy(() => import("../UsersSection")),
|
||||
breadcrumb: (t) => t("users:title"),
|
||||
access: "query-users",
|
||||
|
|
Loading…
Reference in a new issue