diff --git a/src/clients/ClientDetails.tsx b/src/clients/ClientDetails.tsx index cc7d2804e7..e4e158970a 100644 --- a/src/clients/ClientDetails.tsx +++ b/src/clients/ClientDetails.tsx @@ -31,6 +31,7 @@ import { } from "../components/multi-line-input/MultiLineInput"; import { ClientScopes } from "./scopes/ClientScopes"; import { EvaluateScopes } from "./scopes/EvaluateScopes"; +import { RolesList } from "../realm-roles/RolesList"; import { ServiceAccount } from "./service-account/ServiceAccount"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; @@ -108,6 +109,10 @@ export const ClientDetails = () => { const [client, setClient] = useState(); + const loader = async () => { + return await adminClient.clients.listRoles({ id }); + }; + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: "clients:clientDeleteConfirmTitle", messageKey: "clients:clientDeleteConfirm", @@ -216,6 +221,12 @@ export const ClientDetails = () => { )} + {t("roles")}} + > + + {t("clientScopes")}} diff --git a/src/clients/messages.json b/src/clients/messages.json index 440bb542e5..cf8b203e65 100644 --- a/src/clients/messages.json +++ b/src/clients/messages.json @@ -9,6 +9,7 @@ "formatOption": "Format option", "downloadAdaptorTitle": "Download adaptor configs", "credentials": "Credentials", + "roles": "Roles", "clientScopes": "Client scopes", "addClientScope": "Add client scope", "addClientScopesTo": "Add client scopes to {{clientId}}", diff --git a/src/realm-roles/RealmRoleTabs.tsx b/src/realm-roles/RealmRoleTabs.tsx index 1df98ad549..857a642eec 100644 --- a/src/realm-roles/RealmRoleTabs.tsx +++ b/src/realm-roles/RealmRoleTabs.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { useHistory, useParams } from "react-router-dom"; +import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { AlertVariant, ButtonVariant, @@ -18,7 +18,6 @@ import { KeyValueType, RoleAttributes } from "./RoleAttributes"; import { ViewHeader } from "../components/view-header/ViewHeader"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { RealmRoleForm } from "./RealmRoleForm"; -import { useRealm } from "../context/realm-context/RealmContext"; import { AssociatedRolesModal } from "./AssociatedRolesModal"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; @@ -51,10 +50,10 @@ export const RealmRoleTabs = () => { const form = useForm({ mode: "onChange" }); const history = useHistory(); const adminClient = useAdminClient(); - const { realm } = useRealm(); const [role, setRole] = useState(); - const { id } = useParams<{ id: string }>(); + const { id, clientId } = useParams<{ id: string; clientId: string }>(); + const { url } = useRouteMatch(); const { addAlert } = useAlerts(); const [open, setOpen] = useState(false); @@ -94,15 +93,40 @@ export const RealmRoleTabs = () => { if (attributes) { roleRepresentation.attributes = arrayToAttributes(attributes); } - await adminClient.roles.updateById({ id }, roleRepresentation); + if (!clientId) { + await adminClient.roles.updateById({ id }, roleRepresentation); + } else { + await adminClient.clients.updateRole( + { id: clientId, roleName: role.name! }, + roleRepresentation + ); + } setRole(role); } else { - await adminClient.roles.create(roleRepresentation); - const createdRole = await adminClient.roles.findOneByName({ - name: role.name!, - }); + let createdRole; + if (!clientId) { + await adminClient.roles.create(roleRepresentation); + createdRole = await adminClient.roles.findOneByName({ + name: role.name!, + }); + } else { + await adminClient.clients.createRole({ + id: clientId, + name: role.name, + }); + if (role.description) { + await adminClient.clients.updateRole( + { id: clientId, roleName: role.name! }, + roleRepresentation + ); + } + createdRole = await adminClient.clients.findRole({ + id: clientId, + roleName: role.name!, + }); + } setRole(convert(createdRole)); - history.push(`/${realm}/roles/${createdRole.id}`); + history.push(url.substr(0, url.lastIndexOf("/") + 1) + createdRole.id); } addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success); } catch (error) { @@ -124,9 +148,14 @@ export const RealmRoleTabs = () => { continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { - await adminClient.roles.delById({ id }); + if (!clientId) { + await adminClient.roles.delById({ id }); + } else { + await adminClient.clients.delRole({ id: clientId, roleName: name }); + } addAlert(t("roleDeletedSuccess"), AlertVariant.success); - history.replace(`/${realm}/roles`); + const loc = url.replace(/\/attributes/g, ""); + history.replace(`${loc.substr(0, loc.lastIndexOf("/"))}`); } catch (error) { addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger); } diff --git a/src/realm-roles/RealmRolesSection.tsx b/src/realm-roles/RealmRolesSection.tsx index ed409dc35b..26bdaa117d 100644 --- a/src/realm-roles/RealmRolesSection.tsx +++ b/src/realm-roles/RealmRolesSection.tsx @@ -1,32 +1,11 @@ -import React, { useState } from "react"; -import { Link, useHistory, useRouteMatch } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { - AlertVariant, - Button, - ButtonVariant, - PageSection, -} from "@patternfly/react-core"; -import { boolFormatter } from "../util"; -import { useAdminClient } from "../context/auth/AdminClient"; +import React from "react"; +import { PageSection } from "@patternfly/react-core"; import { ViewHeader } from "../components/view-header/ViewHeader"; -import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; -import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; -import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; -import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; -import { useAlerts } from "../components/alert/Alerts"; -import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; -import { emptyFormatter } from "../util"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { RolesList } from "./RolesList"; export const RealmRolesSection = () => { - const { t } = useTranslation("roles"); - const history = useHistory(); const adminClient = useAdminClient(); - const { addAlert } = useAlerts(); - const { url } = useRouteMatch(); - - const [selectedRole, setSelectedRole] = useState(); - const loader = async (to?: number, max?: number, search?: string) => { const params: { [name: string]: string | number } = { to: to!, @@ -35,89 +14,11 @@ export const RealmRolesSection = () => { }; return await adminClient.roles.find(params); }; - - const RoleDetailLink = (role: RoleRepresentation) => ( - <> - - {role.name} - - - ); - - const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ - titleKey: "roles:roleDeleteConfirm", - messageKey: t("roles:roleDeleteConfirmDialog", { - selectedRoleName: selectedRole ? selectedRole!.name : "", - }), - continueButtonLabel: "common:delete", - continueButtonVariant: ButtonVariant.danger, - onConfirm: async () => { - try { - await adminClient.roles.delById({ - id: selectedRole!.id!, - }); - setSelectedRole(undefined); - addAlert(t("roleDeletedSuccess"), AlertVariant.success); - } catch (error) { - addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger); - } - }, - }); - - const goToCreate = () => history.push(`${url}/add-role`); return ( <> - - - - - } - actions={[ - { - title: t("common:delete"), - onRowClick: (role) => { - setSelectedRole(role); - toggleDeleteDialog(); - }, - }, - ]} - columns={[ - { - name: "name", - displayKey: "roles:roleName", - cellRenderer: RoleDetailLink, - cellFormatters: [formattedLinkTableCell(), emptyFormatter()], - }, - { - name: "composite", - displayKey: "roles:composite", - cellFormatters: [boolFormatter(), emptyFormatter()], - }, - { - name: "description", - displayKey: "common:description", - cellFormatters: [emptyFormatter()], - }, - ]} - emptyState={ - - } - /> + ); diff --git a/src/realm-roles/RolesList.tsx b/src/realm-roles/RolesList.tsx new file mode 100644 index 0000000000..0769b0cb83 --- /dev/null +++ b/src/realm-roles/RolesList.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { Link, useHistory, useRouteMatch } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { AlertVariant, Button, ButtonVariant } from "@patternfly/react-core"; + +import { useAdminClient } from "../context/auth/AdminClient"; +import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; +import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; +import { useAlerts } from "../components/alert/Alerts"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { emptyFormatter, boolFormatter } from "../util"; + +type RolesListProps = { + loader: ( + first?: number, + max?: number, + search?: string + ) => Promise; + paginated?: boolean; +}; + +export const RolesList = ({ loader, paginated = true }: RolesListProps) => { + const { t } = useTranslation("roles"); + const history = useHistory(); + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + const { url } = useRouteMatch(); + + const [selectedRole, setSelectedRole] = useState(); + + const RoleDetailLink = (role: RoleRepresentation) => ( + <> + + {role.name} + + + ); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "roles:roleDeleteConfirm", + messageKey: t("roles:roleDeleteConfirmDialog", { + selectedRoleName: selectedRole ? selectedRole!.name : "", + }), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.roles.delById({ + id: selectedRole!.id!, + }); + setSelectedRole(undefined); + addAlert(t("roleDeletedSuccess"), AlertVariant.success); + } catch (error) { + addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger); + } + }, + }); + + const goToCreate = () => history.push(`${url}/add-role`); + return ( + <> + + + + + } + actions={[ + { + title: t("common:delete"), + onRowClick: (role) => { + setSelectedRole(role); + toggleDeleteDialog(); + }, + }, + ]} + columns={[ + { + name: "name", + displayKey: "roles:roleName", + cellRenderer: RoleDetailLink, + cellFormatters: [formattedLinkTableCell(), emptyFormatter()], + }, + { + name: "composite", + displayKey: "roles:composite", + cellFormatters: [boolFormatter(), emptyFormatter()], + }, + { + name: "description", + displayKey: "common:description", + cellFormatters: [emptyFormatter()], + }, + ]} + emptyState={ + + } + /> + + ); +}; diff --git a/src/route-config.ts b/src/route-config.ts index fa51e40b92..2c72f8b288 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -64,6 +64,24 @@ export const routes: RoutesFn = (t: TFunction) => [ breadcrumb: t("clients:clientSettings"), access: "view-clients", }, + { + path: "/:realm/clients/:clientId/roles/add-role", + component: RealmRoleTabs, + breadcrumb: t("roles:createRole"), + access: "manage-realm", + }, + { + path: "/:realm/clients/:clientId/roles/:id", + component: RealmRoleTabs, + breadcrumb: t("roles:roleDetails"), + access: "view-realm", + }, + { + path: "/:realm/clients/:clientId/roles/:id/:tab", + component: RealmRoleTabs, + breadcrumb: null, + access: "view-realm", + }, { path: "/:realm/clients/:id/:tab?/:subtab?", component: ClientDetails, diff --git a/src/user/SearchUser.tsx b/src/user/SearchUser.tsx new file mode 100644 index 0000000000..be7fff3b3a --- /dev/null +++ b/src/user/SearchUser.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { + Button, + ButtonVariant, + EmptyState, + EmptyStateBody, + Form, + InputGroup, + TextInput, + Title, +} from "@patternfly/react-core"; +import { SearchIcon } from "@patternfly/react-icons"; +import { useForm } from "react-hook-form"; + +type SearchUserProps = { + onSearch: (search: string) => void; +}; + +export const SearchUser = ({ onSearch }: SearchUserProps) => { + const { t } = useTranslation("users"); + const { register, handleSubmit } = useForm<{ search: string }>(); + return ( + + + {t("startBySearchingAUser")} + + + {t("startIntro")} +
onSearch(form.search))}> + + + + +
+
+ +
+ ); +};