added join org modal
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
a3ffbb439d
commit
e6dd8ac1c0
5 changed files with 240 additions and 59 deletions
|
@ -3220,4 +3220,14 @@ validatingX509CertsHelp=The public certificates Keycloak uses to validate the si
|
|||
emptyUserOrganizations=No organizations
|
||||
emptyUserOrganizationsInstructions=There is no organization yet. Please join an organization or send an invitation to join an organization.
|
||||
joinOrganization=Join organization
|
||||
sendInvitation=Send invitation
|
||||
sendInvitation=Send invitation
|
||||
removeConfirmOrganizationTitle=Remove organization?
|
||||
organizationRemoveConfirm=Are you sure you want to remove user from the {{count}} selected organizations?
|
||||
organizationRemovedSuccess=User removed from organizations
|
||||
organizationRemoveError=Could not remove user from organizations\: {{error}}
|
||||
organizationName=Organization name
|
||||
joinOrganization=Join organization
|
||||
join=Join
|
||||
userAddedOrganization_one=Organization added to the user
|
||||
userAddedOrganizationError=Could not add organizations to the user\: {{error}}
|
||||
userAddedOrganization_other={{count}} organizations added to the user
|
||||
|
|
74
js/apps/admin-ui/src/organizations/OrganizationModal.tsx
Normal file
74
js/apps/admin-ui/src/organizations/OrganizationModal.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import { Button, Modal, ModalVariant } from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
|
||||
type OrganizationModalProps = {
|
||||
onAdd: (orgs: OrganizationRepresentation[]) => Promise<void>;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const OrganizationModal = ({
|
||||
onAdd,
|
||||
onClose,
|
||||
}: OrganizationModalProps) => {
|
||||
const { adminClient } = useAdminClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
return await adminClient.organizations.find({ first, max, search });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("joinOrganization")}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="join"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
onClick={async () => {
|
||||
await onAdd(selectedRows);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("join")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
key="cancel"
|
||||
variant="link"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="organizationsList"
|
||||
searchPlaceholderKey="searchOrganization"
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: "organizationName",
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -57,17 +57,21 @@ const Domains = (org: OrganizationRepresentation) => {
|
|||
};
|
||||
|
||||
type OrganizationTableProps = PropsWithChildren & {
|
||||
loader:
|
||||
| LoaderFunction<OrganizationRepresentation>
|
||||
| OrganizationRepresentation[];
|
||||
onDelete?: (org: OrganizationRepresentation) => void;
|
||||
loader: LoaderFunction<OrganizationRepresentation>;
|
||||
toolbarItem?: ReactNode;
|
||||
isPaginated?: boolean;
|
||||
onSelect?: (orgs: OrganizationRepresentation[]) => void;
|
||||
onDelete?: (org: OrganizationRepresentation) => void;
|
||||
deleteLabel?: string;
|
||||
};
|
||||
|
||||
export const OrganizationTable = ({
|
||||
loader,
|
||||
toolbarItem,
|
||||
isPaginated = false,
|
||||
onSelect,
|
||||
onDelete,
|
||||
deleteLabel = "delete",
|
||||
children,
|
||||
}: OrganizationTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -75,13 +79,15 @@ export const OrganizationTable = ({
|
|||
return (
|
||||
<KeycloakDataTable
|
||||
loader={loader}
|
||||
isPaginated
|
||||
isPaginated={isPaginated}
|
||||
ariaLabelKey="organizationList"
|
||||
searchPlaceholderKey="searchOrganization"
|
||||
toolbarItem={toolbarItem}
|
||||
onSelect={onSelect}
|
||||
canSelectAll={onSelect !== undefined}
|
||||
actions={[
|
||||
{
|
||||
title: t("delete"),
|
||||
title: t(deleteLabel),
|
||||
onRowClick: onDelete,
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -63,6 +63,7 @@ export default function OrganizationSection() {
|
|||
<OrganizationTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
toolbarItem={
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
|
|
|
@ -1,66 +1,156 @@
|
|||
import { Button, ToolbarItem } from "@patternfly/react-core";
|
||||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import { useAlerts } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { OrganizationModal } from "../organizations/OrganizationModal";
|
||||
import { OrganizationTable } from "../organizations/OrganizationTable";
|
||||
import { toAddOrganization } from "../organizations/routes/AddOrganization";
|
||||
import useToggle from "../utils/useToggle";
|
||||
import { UserParams } from "./routes/User";
|
||||
|
||||
export const Organizations = () => {
|
||||
const { adminClient } = useAdminClient();
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const { id } = useParams<UserParams>();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
const [joinToggle, toggle, setJoinToggle] = useToggle();
|
||||
const [joinOrganization, setJoinOrganization] = useState(false);
|
||||
const [selectedOrgs, setSelectedOrgs] = useState<
|
||||
OrganizationRepresentation[]
|
||||
>([]);
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "removeConfirmOrganizationTitle",
|
||||
messageKey: t("organizationRemoveConfirm", { count: selectedOrgs.length }),
|
||||
continueButtonLabel: "remove",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedOrgs.map((org) =>
|
||||
adminClient.organizations.delMember({
|
||||
orgId: org.id!,
|
||||
userId: id!,
|
||||
}),
|
||||
),
|
||||
);
|
||||
addAlert(t("organizationRemovedSuccess"));
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("organizationRemoveError", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<OrganizationTable
|
||||
loader={() =>
|
||||
adminClient.organizations.memberOrganizations({
|
||||
userId: id!,
|
||||
})
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="joinOrganization"
|
||||
component={(props) => (
|
||||
<Link {...props} to={toAddOrganization({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("joinOrganization")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="removeOrganization"
|
||||
variant="secondary"
|
||||
component={(props) => (
|
||||
<Link {...props} to={toAddOrganization({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ListEmptyState
|
||||
message={t("emptyUserOrganizations")}
|
||||
instructions={t("emptyUserOrganizationsInstructions")}
|
||||
secondaryActions={[
|
||||
{
|
||||
text: t("joinOrganization"),
|
||||
onClick: () => alert("join organization"),
|
||||
},
|
||||
{
|
||||
text: t("sendInvitation"),
|
||||
onClick: () => alert("send invitation"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</OrganizationTable>
|
||||
<>
|
||||
{joinOrganization && (
|
||||
<OrganizationModal
|
||||
onClose={() => setJoinOrganization(false)}
|
||||
onAdd={async (orgs) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
orgs.map((org) =>
|
||||
adminClient.organizations.addMember({
|
||||
orgId: org.id!,
|
||||
userId: id!,
|
||||
}),
|
||||
),
|
||||
);
|
||||
addAlert(t("userAddedOrganization", { count: orgs.length }));
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("userAddedOrganizationError", error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DeleteConfirm />
|
||||
<OrganizationTable
|
||||
key={key}
|
||||
loader={() =>
|
||||
adminClient.organizations.memberOrganizations({
|
||||
userId: id!,
|
||||
})
|
||||
}
|
||||
onSelect={(orgs) => setSelectedOrgs(orgs)}
|
||||
deleteLabel="remove"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
onOpenChange={setJoinToggle}
|
||||
toggle={(ref) => (
|
||||
<MenuToggle
|
||||
ref={ref}
|
||||
id="toggle-id"
|
||||
onClick={toggle}
|
||||
variant="primary"
|
||||
>
|
||||
{t("joinOrganization")}
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={joinToggle}
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem
|
||||
key="join"
|
||||
onClick={() => {
|
||||
setJoinOrganization(true);
|
||||
}}
|
||||
>
|
||||
{t("joinOrganization")}
|
||||
</DropdownItem>
|
||||
<DropdownItem key="invite" component="button">
|
||||
{t("invite")}
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="removeOrganization"
|
||||
variant="secondary"
|
||||
isDisabled={selectedOrgs.length === 0}
|
||||
onClick={() => toggleDeleteDialog()}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ListEmptyState
|
||||
message={t("emptyUserOrganizations")}
|
||||
instructions={t("emptyUserOrganizationsInstructions")}
|
||||
secondaryActions={[
|
||||
{
|
||||
text: t("joinOrganization"),
|
||||
onClick: () => alert("join organization"),
|
||||
},
|
||||
{
|
||||
text: t("sendInvitation"),
|
||||
onClick: () => alert("send invitation"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</OrganizationTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue