Consolidate people and groups tabs to user / user groups shared tab

This commit is contained in:
Tuan Dang
2024-07-11 13:35:11 +07:00
parent 5dff46ee3a
commit 93af7573ac
23 changed files with 1006 additions and 1018 deletions
@@ -3,16 +3,10 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import {
OrgGroupsTab,
OrgIdentityTab,
OrgMembersTab,
OrgRoleTabSection
} from "./components";
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
enum TabSections {
Member = "members",
Groups = "groups",
Roles = "roles",
Identities = "identities"
}
@@ -25,8 +19,7 @@ export const MembersPage = withPermission(
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Organization Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.Groups}>Groups</Tab>
<Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
@@ -37,9 +30,6 @@ export const MembersPage = withPermission(
<TabPanel value={TabSections.Member}>
<OrgMembersTab />
</TabPanel>
<TabPanel value={TabSections.Groups}>
<OrgGroupsTab />
</TabPanel>
<TabPanel value={TabSections.Identities}>
<OrgIdentityTab />
</TabPanel>
@@ -3,16 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useSubscription
} from "@app/context";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -21,100 +13,88 @@ import { OrgGroupModal } from "./OrgGroupModal";
import { OrgGroupsTable } from "./OrgGroupsTable";
export const OrgGroupsSection = () => {
const { subscription } = useSubscription();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"group",
"groupMembers",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description: "You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
const { subscription } = useSubscription();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"group",
"groupMembers",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
const onDeleteGroupSubmit = async ({
name,
};
const onDeleteGroupSubmit = async ({ name, slug }: { name: string; slug: string }) => {
try {
await deleteMutateAsync({
slug
}: {
name: string;
slug: string;
}) => {
try {
await deleteMutateAsync({
slug
});
createNotification({
text: `Successfully deleted the group named ${name}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete the group named ${name}`,
type: "error"
});
}
handlePopUpClose("deleteGroup");
});
createNotification({
text: `Successfully deleted the group named ${name}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete the group named ${name}`,
type: "error"
});
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Group
</Button>
)}
</OrgPermissionCan>
</div>
<OrgGroupsTable
handlePopUpOpen={handlePopUpOpen}
/>
<OrgGroupModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OrgGroupMembersModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteGroupSubmit(
(popUp?.deleteGroup?.data as { name: string; slug: string })
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
}
handlePopUpClose("deleteGroup");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">User Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Group
</Button>
)}
</OrgPermissionCan>
</div>
<OrgGroupsTable handlePopUpOpen={handlePopUpOpen} />
<OrgGroupModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OrgGroupMembersModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; slug: string })
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -1,12 +1,16 @@
import { useState } from "react";
import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Select,
SelectItem,
@@ -17,218 +21,200 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization} from "@app/context";
import {
useGetOrganizationGroups,
useGetOrgRoles,
useUpdateGroup
} from "@app/hooks/api";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["group", "deleteGroup", "groupMembers"]
>,
data?: {
groupId?: string;
name?: string;
slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
}
}
) => void;
};
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["group", "deleteGroup", "groupMembers"]>,
data?: {
groupId?: string;
name?: string;
slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
}
) => void;
};
export const OrgGroupsTable = ({
handlePopUpOpen
}: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const { data: roles } = useGetOrgRoles(orgId);
const handleChangeRole = async ({
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const { data: roles } = useGetOrgRoles(orgId);
const handleChangeRole = async ({ currentSlug, role }: { currentSlug: string; role: string }) => {
try {
await updateMutateAsync({
currentSlug,
role
}: {
currentSlug: string;
role: string;
}) => {
try {
await updateMutateAsync({
currentSlug,
role
});
createNotification({
text: "Successfully updated group role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update group role",
type: "error"
});
}
});
createNotification({
text: "Successfully updated group role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update group role",
type: "error"
});
}
return (
<div>
<Input
value={searchGroupsFilter}
onChange={(e) => setSearchGroupsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading && groups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<div className="flex items-center justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Manage group members">
<IconButton
onClick={() => {
handlePopUpOpen("groupMembers", {
slug
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faUsers} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Edit group">
<IconButton
onClick={async () => {
handlePopUpOpen("group", {
groupId: id,
name,
slug,
role,
customRole
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Delete group">
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{groups?.length === 0 && (
<EmptyState title="No groups found" icon={faUsers} />
)}
</TableContainer>
</div>
);
}
};
return (
<div>
<Input
value={searchGroupsFilter}
onChange={(e) => setSearchGroupsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading &&
groups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("groupMembers", {
slug
});
}}
disabled={!isAllowed}
>
Manage Users
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("group", {
groupId: id,
name,
slug,
role,
customRole
});
}}
disabled={!isAllowed}
>
Edit Group
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
disabled={!isAllowed}
>
Delete Group
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{groups?.length === 0 && <EmptyState title="No groups found" icon={faUsers} />}
</TableContainer>
</div>
);
};
@@ -1,17 +1,19 @@
import { motion } from "framer-motion";
import { OrgGroupsSection } from "../OrgGroupsTab/components";
import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => {
return (
<motion.div
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersSection />
<OrgMembersSection />
<OrgGroupsSection />
</motion.div>
);
}
};
@@ -90,7 +90,7 @@ export const OrgMembersSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button