improvements to user group ui

This commit is contained in:
Daniel Hougaard
2024-09-08 20:20:10 +04:00
parent 655ee4f118
commit e1cd632546
4 changed files with 189 additions and 152 deletions
@@ -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}
/>
</>
);
};
@@ -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}