mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 20:26:44 +00:00
Merge pull request #735 from Infisical/new-sidebars
fixing the bugs with sidebars
This commit is contained in:
@@ -66,7 +66,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
{icon && <span className="flex items-center mr-2">{icon}</span>}
|
{icon && <span className="flex items-center mr-2">{icon}</span>}
|
||||||
<span className="flex-grow text-left">{children}</span>
|
<span className="flex-grow text-left">{children}</span>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createContext, ReactNode, useContext, useMemo } from "react";
|
|||||||
import { useGetOrganization } from "@app/hooks/api";
|
import { useGetOrganization } from "@app/hooks/api";
|
||||||
import { Organization } from "@app/hooks/api/types";
|
import { Organization } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { useWorkspace } from "../WorkspaceContext";
|
|
||||||
|
|
||||||
type TOrgContext = {
|
type TOrgContext = {
|
||||||
orgs?: Organization[];
|
orgs?: Organization[];
|
||||||
@@ -18,10 +17,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OrgProvider = ({ children }: Props): JSX.Element => {
|
export const OrgProvider = ({ children }: Props): JSX.Element => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { data: userOrgs, isLoading } = useGetOrganization();
|
const { data: userOrgs, isLoading } = useGetOrganization();
|
||||||
|
|
||||||
const currentWsOrgID = currentWorkspace?.organization;
|
// const currentWsOrgID = currentWorkspace?.organization;
|
||||||
|
const currentWsOrgID = localStorage.getItem("orgData.id");
|
||||||
|
|
||||||
// memorize the workspace details for the context
|
// memorize the workspace details for the context
|
||||||
const value = useMemo<TOrgContext>(
|
const value = useMemo<TOrgContext>(
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeOrg = async (orgId) => {
|
||||||
|
localStorage.setItem("orgData.id", orgId);
|
||||||
|
router.push(`/org/${orgId}/overview`)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
||||||
// Placing the localstorage as much as possible
|
// Placing the localstorage as much as possible
|
||||||
// Wait till tony integrates the azure and its launched
|
// Wait till tony integrates the azure and its launched
|
||||||
@@ -158,7 +163,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
if (currentOrg && (
|
if (currentOrg && (
|
||||||
(workspaces?.length === 0 && router.asPath.includes("project"))
|
(workspaces?.length === 0 && router.asPath.includes("project"))
|
||||||
|| router.asPath.includes("/project/undefined")
|
|| router.asPath.includes("/project/undefined")
|
||||||
|| (!orgs?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("integration"))
|
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration"))
|
||||||
)) {
|
)) {
|
||||||
router.push(`/org/${currentOrg?._id}/overview`);
|
router.push(`/org/${currentOrg?._id}/overview`);
|
||||||
}
|
}
|
||||||
@@ -257,8 +262,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
||||||
{orgs?.map(org => <DropdownMenuItem key={org._id}>
|
{orgs?.map(org => <DropdownMenuItem key={org._id}>
|
||||||
<Link href={`/org/${org._id}/overview`}><div className="w-full flex justify-between items-center">{org.name}{currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="ml-auto text-primary"/>}</div></Link>
|
<Button
|
||||||
</DropdownMenuItem>)}
|
onClick={() => changeOrg(org?._id)}
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
size="xs"
|
||||||
|
className="w-full flex items-center justify-start p-0 font-normal"
|
||||||
|
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>}
|
||||||
|
>
|
||||||
|
<div className="w-full flex justify-between items-center">{org.name}</div>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -270,8 +285,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600 p-1">
|
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1">
|
||||||
<div className="w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-400 pr-1 text-xs text-mineshaft-300 flex justify-center">
|
<div className="child w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-500 pr-1 text-xs text-mineshaft-300 flex justify-center items-center">
|
||||||
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)}
|
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -354,10 +369,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : <div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm">
|
) : <Link href={`/org/${currentOrg?._id}/overview`}><div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm">
|
||||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/>
|
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/>
|
||||||
<Link href={`/org/${currentOrg?._id}/overview`}>Back to organization</Link>
|
Back to organization
|
||||||
</div>)}
|
</div></Link>)}
|
||||||
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||||
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
|
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
|
||||||
@@ -391,13 +406,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||||
<MenuItem
|
<a>
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
<MenuItem
|
||||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
||||||
icon="system-outline-168-view-headline"
|
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||||
>
|
icon="system-outline-168-view-headline"
|
||||||
Audit Logs
|
>
|
||||||
</MenuItem>
|
Audit Logs
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
@@ -442,7 +459,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
isSelected={router.asPath === `/org/${currentOrg?._id}/members`}
|
isSelected={router.asPath === `/org/${currentOrg?._id}/members`}
|
||||||
icon="system-outline-96-groups"
|
icon="system-outline-96-groups"
|
||||||
>
|
>
|
||||||
Organization Members
|
Members
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<WorkspaceProvider>
|
<OrgProvider>
|
||||||
<OrgProvider>
|
<WorkspaceProvider>
|
||||||
<SubscriptionProvider>
|
<SubscriptionProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
@@ -90,8 +90,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</SubscriptionProvider>
|
</SubscriptionProvider>
|
||||||
</OrgProvider>
|
</WorkspaceProvider>
|
||||||
</WorkspaceProvider>
|
</OrgProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -133,6 +133,61 @@ const learningItem = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const learningItemSquare = ({
|
||||||
|
text,
|
||||||
|
subText,
|
||||||
|
complete,
|
||||||
|
icon,
|
||||||
|
time,
|
||||||
|
userAction,
|
||||||
|
link
|
||||||
|
}: ItemProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
target={`${link?.includes("https") ? "_blank" : "_self"}`}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
|
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}>
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={async () => {
|
||||||
|
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||||
|
await registerUserAction({
|
||||||
|
action: userAction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center w-full px-6 py-4">
|
||||||
|
<div className="flex flex-row items-start justify-between w-full">
|
||||||
|
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" />
|
||||||
|
{complete && (
|
||||||
|
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`}
|
||||||
|
>
|
||||||
|
{complete ? "Complete!" : `About ${time}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start justify-start w-full pt-4">
|
||||||
|
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
||||||
|
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup.string().required().label("Project Name").trim(),
|
name: yup.string().required().label("Project Name").trim(),
|
||||||
addMembers: yup.bool().required().label("Add Members")
|
addMembers: yup.bool().required().label("Add Members")
|
||||||
@@ -147,9 +202,8 @@ export default function Organization() {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaces,
|
const { workspaces } = useWorkspace();
|
||||||
// isLoading: isWorkspaceLoading
|
const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || []
|
||||||
} = useWorkspace();
|
|
||||||
const currentOrg = String(router.query.id);
|
const currentOrg = String(router.query.id);
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const addWsUser = useAddUserToWs();
|
const addWsUser = useAddUserToWs();
|
||||||
@@ -267,16 +321,16 @@ export default function Organization() {
|
|||||||
Add New Project
|
Add New Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full grid gap-4 grid-cols-3 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
{workspaces.filter(ws => ws.name.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-32 lg:h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
||||||
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
||||||
<div className="text-sm text-mineshaft-300 mt-0 lg:pb-6">{(workspace.environments?.length || 0)} environments</div>
|
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
|
||||||
<Link href={`/project/${workspace._id}/secrets`}>
|
<Link href={`/project/${workspace._id}/secrets`}>
|
||||||
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
|
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
{workspaces.length === 0 && (
|
{orgWorkspaces.length === 0 && (
|
||||||
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
||||||
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
||||||
<div className="text-center font-light">
|
<div className="text-center font-light">
|
||||||
@@ -291,25 +345,44 @@ export default function Organization() {
|
|||||||
</div>
|
</div>
|
||||||
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||||
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
||||||
{learningItem({
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3">
|
||||||
text: "Watch a video about Infisical",
|
{learningItemSquare({
|
||||||
subText: "",
|
text: "Watch Infisical demo",
|
||||||
complete: hasUserClickedIntro,
|
subText: "Set up Infisical in 3 min.",
|
||||||
icon: faHandPeace,
|
complete: hasUserClickedIntro,
|
||||||
time: "3 min",
|
icon: faHandPeace,
|
||||||
userAction: "intro_cta_clicked",
|
time: "3 min",
|
||||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
userAction: "intro_cta_clicked",
|
||||||
})}
|
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||||
{workspaces.length !== 0 && learningItem({
|
})}
|
||||||
text: "Add your secrets",
|
{orgWorkspaces.length !== 0 && learningItemSquare({
|
||||||
subText: "Click to see example secrets, and add your own.",
|
text: "Add your secrets",
|
||||||
complete: hasUserPushedSecrets,
|
subText: "Drop a .env file or type your secrets.",
|
||||||
icon: faPlus,
|
complete: hasUserPushedSecrets,
|
||||||
time: "1 min",
|
icon: faPlus,
|
||||||
userAction: "first_time_secrets_pushed",
|
time: "1 min",
|
||||||
link: `/project/${workspaces[0]?._id}/secrets`
|
userAction: "first_time_secrets_pushed",
|
||||||
})}
|
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
||||||
{workspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
})}
|
||||||
|
{learningItemSquare({
|
||||||
|
text: "Invite your teammates",
|
||||||
|
subText: "Infisical is better used as a team.",
|
||||||
|
complete: usersInOrg,
|
||||||
|
icon: faUserPlus,
|
||||||
|
time: "2 min",
|
||||||
|
link: `/org/${router.query.id}/members?action=invite`
|
||||||
|
})}
|
||||||
|
<div className="block xl:hidden 2xl:block">{learningItemSquare({
|
||||||
|
text: "Join Infisical Slack",
|
||||||
|
subText: "Have any questions? Ask us!",
|
||||||
|
complete: hasUserClickedSlack,
|
||||||
|
icon: faSlack,
|
||||||
|
time: "1 min",
|
||||||
|
userAction: "slack_cta_clicked",
|
||||||
|
link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ"
|
||||||
|
})}</div>
|
||||||
|
</div>
|
||||||
|
{orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
||||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||||
<div className="mr-4 flex w-full flex-row items-center">
|
<div className="mr-4 flex w-full flex-row items-center">
|
||||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||||
@@ -332,7 +405,7 @@ export default function Organization() {
|
|||||||
<TabsObject />
|
<TabsObject />
|
||||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||||
</div>}
|
</div>}
|
||||||
{workspaces.length !== 0 && learningItem({
|
{orgWorkspaces.length !== 0 && learningItem({
|
||||||
text: "Integrate Infisical with your infrastructure",
|
text: "Integrate Infisical with your infrastructure",
|
||||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||||
complete: false,
|
complete: false,
|
||||||
@@ -340,23 +413,6 @@ export default function Organization() {
|
|||||||
time: "15 min",
|
time: "15 min",
|
||||||
link: "https://infisical.com/docs/integrations/overview"
|
link: "https://infisical.com/docs/integrations/overview"
|
||||||
})}
|
})}
|
||||||
{learningItem({
|
|
||||||
text: "Invite your teammates",
|
|
||||||
subText: "",
|
|
||||||
complete: usersInOrg,
|
|
||||||
icon: faUserPlus,
|
|
||||||
time: "2 min",
|
|
||||||
link: `/org/${router.query.id}/members?action=invite`
|
|
||||||
})}
|
|
||||||
{learningItem({
|
|
||||||
text: "Join Infisical Slack",
|
|
||||||
subText: "Have any questions? Ask us!",
|
|
||||||
complete: hasUserClickedSlack,
|
|
||||||
icon: faSlack,
|
|
||||||
time: "1 min",
|
|
||||||
userAction: "slack_cta_clicked",
|
|
||||||
link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ"
|
|
||||||
})}
|
|
||||||
</div>}
|
</div>}
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
||||||
<p className="mr-4 font-semibold text-white">Explore More</p>
|
<p className="mr-4 font-semibold text-white">Explore More</p>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr,
|
||||||
UpgradePlanModal} from "@app/components/v2";
|
UpgradePlanModal} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { OrgUser, Workspace } from "@app/hooks/api/types";
|
import { OrgUser, Workspace } from "@app/hooks/api/types";
|
||||||
@@ -70,6 +71,7 @@ export const OrgMembersTable = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
const {data: serverDetails } = useFetchServerStatus()
|
||||||
|
const { workspaces } = useWorkspace();
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addMember",
|
"addMember",
|
||||||
@@ -230,7 +232,7 @@ export const OrgMembersTable = ({
|
|||||||
: <Tag colorSchema="red">This user isn't part of any projects yet</Tag>}
|
: <Tag colorSchema="red">This user isn't part of any projects yet</Tag>}
|
||||||
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
|
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push(`/users/${router.query.id}`)}
|
onClick={() => router.push(`/project/${workspaces[0]?._id}/members`)}
|
||||||
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm'
|
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm'
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
||||||
|
|||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization } from "@app/context";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
useAddUserToOrg
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
|
||||||
email: yup.string().email().required().label("Email").trim()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>; // TODO: change to FormData
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
popUp: UsePopUpState<["addMember"]>;
|
|
||||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addMember"]>) => void;
|
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: test no-SMTP setup case
|
|
||||||
|
|
||||||
export const AddOrgMemberModal = ({
|
|
||||||
popUp,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpClose
|
|
||||||
}: Props) => {
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
|
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
|
||||||
const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>("");
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset
|
|
||||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAddUserToOrg();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
if (isInviteLinkCopied) {
|
|
||||||
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isInviteLinkCopied]);
|
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(completeInviteLink as string);
|
|
||||||
setInviteLinkCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TAddMemberForm) => {
|
|
||||||
try {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
const { data } = await mutateAsync({
|
|
||||||
organizationId: currentOrg?._id,
|
|
||||||
inviteeEmail: email
|
|
||||||
});
|
|
||||||
|
|
||||||
setCompleteInviteLink(data?.completeInviteLink);
|
|
||||||
|
|
||||||
if (!data.completeInviteLink) {
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully sent an invite to the user.",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverDetails?.emailConfigured){
|
|
||||||
handlePopUpClose("addMember");
|
|
||||||
}
|
|
||||||
|
|
||||||
reset();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to send an invite to the user",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addMember?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addMember", isOpen);
|
|
||||||
setCompleteInviteLink(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title={`Invite others to ${currentOrg?.name ?? ""}`}
|
|
||||||
subTitle={
|
|
||||||
<div>
|
|
||||||
{!completeInviteLink && <div>
|
|
||||||
An invite is specific to an email address and expires after 1 day.
|
|
||||||
<br />
|
|
||||||
For security reasons, you will need to separately add members to projects.
|
|
||||||
</div>}
|
|
||||||
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!completeInviteLink && (
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} >
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="email"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpClose("addMember")}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
{completeInviteLink && (
|
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{completeInviteLink}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useSubscription } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
|
|
||||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
|
||||||
import { OrgMembersTable } from "./OrgMembersTable";
|
|
||||||
|
|
||||||
export const OrgMembersSection = () => {
|
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addMember",
|
|
||||||
"upgradePlan",
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const isMoreMembersAllowed = subscription?.memberLimit ? (subscription.membersUsed < subscription.memberLimit) : true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
|
||||||
<div className="flex justify-between mb-4">
|
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
|
||||||
Organization members
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
if (isMoreMembersAllowed) {
|
|
||||||
handlePopUpOpen("addMember");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<OrgMembersTable />
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text="Add more members by upgrading to a higher Infisical plan."
|
|
||||||
/>
|
|
||||||
<AddOrgMemberModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
handlePopUpClose={handlePopUpClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-304
@@ -1,304 +0,0 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmptyState,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TableSkeleton,
|
|
||||||
Tag,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useUser, useWorkspace } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
useAddUserToOrg,
|
|
||||||
useDeleteOrgMembership,
|
|
||||||
useGetOrgUsers,
|
|
||||||
useGetUserWorkspaceMemberships,
|
|
||||||
useGetUserWsKey,
|
|
||||||
useUpdateOrgUserRole,
|
|
||||||
useUploadWsKey
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
|
||||||
|
|
||||||
export const OrgMembersTable = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { user: currentUser } = useUser();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
|
||||||
|
|
||||||
const { data: serverDetails } = useFetchServerStatus()
|
|
||||||
const { data: members, isLoading: isOrgUserLoading } = useGetOrgUsers(currentOrg?._id ?? ""); // members
|
|
||||||
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(currentOrg?._id ?? "");
|
|
||||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || "");
|
|
||||||
|
|
||||||
const uploadWsKey = useUploadWsKey();
|
|
||||||
const addUserToOrg = useAddUserToOrg();
|
|
||||||
const removeUserOrgMembership = useDeleteOrgMembership();
|
|
||||||
const updateOrgUserRole = useUpdateOrgUserRole();
|
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"removeMember",
|
|
||||||
"setUpEmail"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const isLoading = isOrgUserLoading || IsWsMembershipLoading;
|
|
||||||
const userId = currentUser?._id || "";
|
|
||||||
const onRemoveMember = async (membershipId: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully removed user from org",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove user from the organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveOrgMemberApproved = async () => {
|
|
||||||
const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
|
||||||
await onRemoveMember(orgMembershipId);
|
|
||||||
handlePopUpClose("removeMember");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const onInviteMember = async (email: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await addUserToOrg.mutateAsync({
|
|
||||||
organizationId: currentOrg?._id,
|
|
||||||
inviteeEmail: email
|
|
||||||
});
|
|
||||||
|
|
||||||
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
|
|
||||||
if (!data.completeInviteLink) {
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully invited user to the organization.",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to invite user to org",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGrantAccess = async (targetUserId: string, publicKey: string) => {
|
|
||||||
try {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
|
||||||
if (!PRIVATE_KEY || !wsKey) return;
|
|
||||||
|
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
|
||||||
const key = decryptAssymmetric({
|
|
||||||
ciphertext: wsKey.encryptedKey,
|
|
||||||
nonce: wsKey.nonce,
|
|
||||||
publicKey: wsKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadWsKey.mutateAsync({
|
|
||||||
userId: targetUserId,
|
|
||||||
nonce,
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
workspaceId: currentWorkspace?._id || ""
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to grant access to user",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRoleChange = async (membershipId: string, role: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated user role",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update user role",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isIamOwner = useMemo(
|
|
||||||
() => members ? members.find(({ user }) => userId === user?._id)?.role === "owner" : [],
|
|
||||||
[userId, members]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterdUser = useMemo(
|
|
||||||
() =>
|
|
||||||
members ? members.filter(
|
|
||||||
({ user, inviteEmail }) =>
|
|
||||||
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
inviteEmail?.includes(searchMemberFilter)
|
|
||||||
) : [],
|
|
||||||
[members, searchMemberFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Input
|
|
||||||
value={searchMemberFilter}
|
|
||||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
placeholder="Search members..."
|
|
||||||
/>
|
|
||||||
<TableContainer className="mt-4">
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>Email</Th>
|
|
||||||
<Th>Role</Th>
|
|
||||||
<Th>Projects</Th>
|
|
||||||
<Th aria-label="actions" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isLoading && <TableSkeleton columns={5} key="org-memberships" />}
|
|
||||||
{!isLoading &&
|
|
||||||
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
|
||||||
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
|
||||||
const email = user?.email || inviteEmail;
|
|
||||||
const userWs = workspaceMemberships?.[user?._id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>{email}</Td>
|
|
||||||
<Td>
|
|
||||||
{status === "accepted" && (
|
|
||||||
<Select
|
|
||||||
defaultValue={role}
|
|
||||||
isDisabled={userId === user?._id}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={(selectedRole) =>
|
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(isIamOwner || role === "owner") && (
|
|
||||||
<SelectItem value="owner">owner</SelectItem>
|
|
||||||
)}
|
|
||||||
<SelectItem value="admin">admin</SelectItem>
|
|
||||||
<SelectItem value="member">member</SelectItem>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && (
|
|
||||||
<Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}>
|
|
||||||
Resend Invite
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{status === "completed" && (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
|
||||||
>
|
|
||||||
Grant Access
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{userWs ? (
|
|
||||||
userWs?.map(({ name: wsName, _id }) => (
|
|
||||||
<Tag key={`user-${currentUser._id}-workspace-${_id}`} className="my-1">
|
|
||||||
{wsName}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-row'>
|
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured)
|
|
||||||
? <Tag colorSchema="red">This user hasn't accepted the invite yet</Tag>
|
|
||||||
: <Tag colorSchema="red">This user isn't part of any projects yet</Tag>}
|
|
||||||
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
|
|
||||||
type="button"
|
|
||||||
onClick={() => router.push(`/users/${router.query.id}`)}
|
|
||||||
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm'
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
||||||
Add to projects
|
|
||||||
</button>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{userId !== user?._id && (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
isDisabled={userId === user?._id}
|
|
||||||
onClick={() => handlePopUpOpen("removeMember", { id: orgMembershipId })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isLoading && filterdUser?.length === 0 && (
|
|
||||||
<EmptyState title="No project members found" icon={faUsers} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeMember.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this user from the org?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveOrgMemberApproved}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { OrgMembersSection } from "./OrgMembersSection";
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
||||||
export { OrgMembersSection } from "./OrgMembersSection";
|
|
||||||
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
||||||
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
||||||
Reference in New Issue
Block a user