mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 18:25:45 +00:00
improvements to user group ui
This commit is contained in:
+160
@@ -0,0 +1,160 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { faFolder, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
|
||||
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
export type UserGroupsProjectsModalData = {
|
||||
group: TGroupWithProjectMemberships;
|
||||
groupSlug: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export const UserGroupsProjectsModal = ({
|
||||
popUp,
|
||||
handlePopUpToggle
|
||||
}: {
|
||||
popUp: UsePopUpState<["userGroupsProjects"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["userGroupsProjects"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
}) => {
|
||||
const popupData = popUp.userGroupsProjects.data as UserGroupsProjectsModalData;
|
||||
|
||||
const [filteredProjectName, setFilteredProjectName] = useState("");
|
||||
|
||||
const { mutateAsync: deleteGroupFromProject } = useDeleteGroupFromWorkspace();
|
||||
|
||||
const filteredMemberships = useMemo(() => {
|
||||
if (!popupData?.group) return [];
|
||||
|
||||
return popupData.group.projectMemberships.filter(({ project }) => {
|
||||
return project.name.toLowerCase().includes(filteredProjectName.toLowerCase());
|
||||
});
|
||||
}, [filteredProjectName, popupData?.group?.projectMemberships]);
|
||||
|
||||
const removeProjectFromGroup = useCallback(
|
||||
async (projectSlug: string) => {
|
||||
await deleteGroupFromProject({
|
||||
groupSlug: popupData.groupSlug,
|
||||
projectSlug,
|
||||
username: popupData.username
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Project removed from group"
|
||||
});
|
||||
|
||||
// Manually remove the project from the list, since the query invalidation won't trigger the popup data to be updated
|
||||
popupData.group.projectMemberships = popupData.group.projectMemberships.filter(
|
||||
(membership) => membership.project.slug !== projectSlug
|
||||
);
|
||||
},
|
||||
[popupData]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp.userGroupsProjects.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle("userGroupsProjects", open);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Manage Group Projects">
|
||||
<Input
|
||||
value={filteredProjectName}
|
||||
onChange={(e) => setFilteredProjectName(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search projects..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Project</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody className="w-full">
|
||||
{filteredMemberships?.map(({ project, roles }) => {
|
||||
return (
|
||||
<Tr
|
||||
className="flex w-full items-center justify-between"
|
||||
key={`group-project-${project.id}`}
|
||||
>
|
||||
<Td>
|
||||
<Link href={`/project/${project.id}/secrets/overview`}>
|
||||
<p className="cursor-pointer font-medium capitalize transition-all hover:text-primary-500">
|
||||
{project.name}
|
||||
</p>
|
||||
</Link>
|
||||
{/* If there's only one role, display the role.name. If more, display role.name (+X more) */}
|
||||
<Tooltip
|
||||
isDisabled={roles.length === 1}
|
||||
content={roles
|
||||
.map((role) => role.role.charAt(0).toUpperCase() + role.role.slice(1))
|
||||
.join(", ")}
|
||||
>
|
||||
<p className="text-xs capitalize">
|
||||
{roles.length === 1
|
||||
? roles[0].role
|
||||
: `${roles[0].role} (+${roles.length - 1} more)`}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td className="flex h-full items-center justify-end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Groups}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => removeProjectFromGroup(project.slug)}
|
||||
>
|
||||
Remove group membership
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!filteredMemberships.length && <EmptyState title="No projects found" icon={faFolder} />}
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,161 +1,36 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { faFolder, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
|
||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
||||
import { OrgUser } from "@app/hooks/api/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { UserGroupsProjectsModalData } from "./UserGroupProjectsModal";
|
||||
|
||||
type Props = {
|
||||
group: TGroupWithProjectMemberships;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
||||
orgMembership: OrgUser;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>,
|
||||
data?: {}
|
||||
) => void;
|
||||
};
|
||||
|
||||
const GroupProjectsModal = ({
|
||||
isOpen,
|
||||
group,
|
||||
groupSlug,
|
||||
setIsOpen
|
||||
}: {
|
||||
setIsOpen: (open: boolean) => void;
|
||||
isOpen: boolean;
|
||||
group: TGroupWithProjectMemberships;
|
||||
groupSlug: string;
|
||||
}) => {
|
||||
const [filteredProjectName, setFilteredProjectName] = useState("");
|
||||
|
||||
const { mutateAsync: deleteGroupFromProject } = useDeleteGroupFromWorkspace();
|
||||
|
||||
const filteredMemberships = useMemo(() => {
|
||||
return group.projectMemberships.filter(({ project }) => {
|
||||
return project.name.toLowerCase().includes(filteredProjectName.toLowerCase());
|
||||
});
|
||||
}, [filteredProjectName, group]);
|
||||
|
||||
const removeProjectFromGroup = useCallback(async (projectSlug: string) => {
|
||||
await deleteGroupFromProject({
|
||||
groupSlug,
|
||||
projectSlug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Project removed from group"
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpen(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Manage Group Projects">
|
||||
<Input
|
||||
value={filteredProjectName}
|
||||
onChange={(e) => setFilteredProjectName(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search projects..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Project</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody className="w-full">
|
||||
{filteredMemberships?.map(({ project, roles }) => {
|
||||
return (
|
||||
<Tr
|
||||
className="flex w-full items-center justify-between"
|
||||
key={`group-project-${project.id}`}
|
||||
>
|
||||
<Td>
|
||||
<Link href={`/project/${project.id}`}>
|
||||
<p className="cursor-pointer font-medium capitalize transition-all hover:text-primary-500">
|
||||
{project.name}
|
||||
</p>
|
||||
</Link>
|
||||
{/* If there's only one role, display the role.name. If more, display role.name (+X more) */}
|
||||
<Tooltip
|
||||
isDisabled={roles.length === 1}
|
||||
content={roles
|
||||
.map((role) => role.role.charAt(0).toUpperCase() + role.role.slice(1))
|
||||
.join(", ")}
|
||||
>
|
||||
<p className="text-xs capitalize">
|
||||
{roles.length === 1
|
||||
? roles[0].role
|
||||
: `${roles[0].role} (+${roles.length - 1} more)`}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td className="flex h-full items-center justify-end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Groups}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => removeProjectFromGroup(project.slug)}
|
||||
>
|
||||
Remove group from project
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!filteredMemberships.length && <EmptyState title="No projects found" icon={faFolder} />}
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
|
||||
const [isProjectsModalOpen, setIsProjectsModalOpen] = useState(false);
|
||||
|
||||
export const UserGroupsRow = ({ group, orgMembership, handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`user-project-membership-${group.id}`}
|
||||
onClick={() => setIsProjectsModalOpen(true)}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("userGroupsProjects", {
|
||||
groupSlug: group.slug,
|
||||
username: orgMembership.user.username,
|
||||
group
|
||||
} as UserGroupsProjectsModalData);
|
||||
}}
|
||||
>
|
||||
<Td>{group.name}</Td>
|
||||
<Td>
|
||||
@@ -185,12 +60,6 @@ export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
<GroupProjectsModal
|
||||
groupSlug={group.slug}
|
||||
isOpen={isProjectsModalOpen}
|
||||
setIsOpen={setIsProjectsModalOpen}
|
||||
group={group}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-1
@@ -6,6 +6,7 @@ import { useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { OrgUser } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { UserGroupsProjectsModal } from "./UserGroupProjectsModal";
|
||||
import { UserGroupsTable } from "./UserGroupsTable";
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +15,8 @@ type Props = {
|
||||
|
||||
export const UserGroupsSection = ({ orgMembership }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"removeUserFromGroup"
|
||||
"removeUserFromGroup",
|
||||
"userGroupsProjects"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup();
|
||||
@@ -63,6 +65,8 @@ export const UserGroupsSection = ({ orgMembership }: Props) => {
|
||||
return handleRemoveUserFromGroup(popupData.groupSlug);
|
||||
}}
|
||||
/>
|
||||
|
||||
<UserGroupsProjectsModal handlePopUpToggle={handlePopUpToggle} popUp={popUp} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,10 @@ import { UserGroupsRow } from "./UserGroupsRow";
|
||||
|
||||
type Props = {
|
||||
orgMembership: OrgUser;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>,
|
||||
data?: {}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||
@@ -28,6 +31,7 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||
<TBody>
|
||||
{groups?.map((group) => (
|
||||
<UserGroupsRow
|
||||
orgMembership={orgMembership}
|
||||
key={`user-group-${group.id}`}
|
||||
group={group}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
|
||||
Reference in New Issue
Block a user