mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 20:25:47 +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";
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import Link from "next/link";
|
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faFolder, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
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 { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
||||||
|
import { OrgUser } from "@app/hooks/api/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { UserGroupsProjectsModalData } from "./UserGroupProjectsModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: TGroupWithProjectMemberships;
|
group: TGroupWithProjectMemberships;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
orgMembership: OrgUser;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>,
|
||||||
|
data?: {}
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupProjectsModal = ({
|
export const UserGroupsRow = ({ group, orgMembership, handlePopUpOpen }: Props) => {
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
key={`user-project-membership-${group.id}`}
|
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>{group.name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -185,12 +60,6 @@ export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</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 { OrgUser } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { UserGroupsProjectsModal } from "./UserGroupProjectsModal";
|
||||||
import { UserGroupsTable } from "./UserGroupsTable";
|
import { UserGroupsTable } from "./UserGroupsTable";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -14,7 +15,8 @@ type Props = {
|
|||||||
|
|
||||||
export const UserGroupsSection = ({ orgMembership }: Props) => {
|
export const UserGroupsSection = ({ orgMembership }: Props) => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"removeUserFromGroup"
|
"removeUserFromGroup",
|
||||||
|
"userGroupsProjects"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup();
|
const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup();
|
||||||
@@ -63,6 +65,8 @@ export const UserGroupsSection = ({ orgMembership }: Props) => {
|
|||||||
return handleRemoveUserFromGroup(popupData.groupSlug);
|
return handleRemoveUserFromGroup(popupData.groupSlug);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UserGroupsProjectsModal handlePopUpToggle={handlePopUpToggle} popUp={popUp} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { UserGroupsRow } from "./UserGroupsRow";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
orgMembership: OrgUser;
|
orgMembership: OrgUser;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>,
|
||||||
|
data?: {}
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||||
@@ -28,6 +31,7 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
|||||||
<TBody>
|
<TBody>
|
||||||
{groups?.map((group) => (
|
{groups?.map((group) => (
|
||||||
<UserGroupsRow
|
<UserGroupsRow
|
||||||
|
orgMembership={orgMembership}
|
||||||
key={`user-group-${group.id}`}
|
key={`user-group-${group.id}`}
|
||||||
group={group}
|
group={group}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user