diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 3f9355863..8a8d832cf 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -1,3 +1,5 @@ +// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex + import crypto from "crypto"; import { useEffect, useState } from "react"; @@ -11,8 +13,11 @@ import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faFolderOpen } from "@fortawesome/free-regular-svg-icons"; import { faArrowRight, + faArrowUpRightFromSquare, + faCheck, faCheckCircle, faExclamationCircle, + faClipboard, faHandPeace, faMagnifyingGlass, faNetworkWired, @@ -22,6 +27,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; +import * as Tabs from "@radix-ui/react-tabs"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; @@ -36,7 +42,6 @@ import { Skeleton, UpgradePlanModal } from "@app/components/v2"; -import { TabsObject } from "@app/components/v2/Tabs"; import { useSubscription, useUser, useWorkspace } from "@app/context"; import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; @@ -63,6 +68,210 @@ type ItemProps = { link?: string; }; +function copyToClipboard(id: string, setState: (value: boolean) => void) { + // Get the text field + const copyText = document.getElementById(id) as HTMLInputElement; + + // Select the text field + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.value); + + setState(true); + setTimeout(() => setState(false), 2000); + // Alert the copied text + // alert("Copied the text: " + copyText.value); +} + +const CodeItem = ({ + isCopied, + setIsCopied, + textExplanation, + code, + id +}: { + isCopied: boolean; + setIsCopied: (value: boolean) => void; + textExplanation: string; + code: string; + id: string; +}) => { + return ( + <> +

{textExplanation}

+
+ + +
+ + ); +}; + +const TabsObject = () => { + const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); + const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); + const [loginCodeCopied, setLoginCodeCopied] = useState(false); + const [initCodeCopied, setInitCodeCopied] = useState(false); + const [runCodeCopied, setRunCodeCopied] = useState(false); + + return ( + + + + MacOS + + + Windows + + {/* + Arch Linux + */} + + Other Platforms + + + + + + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+ + +
+ + +
+ + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+
+ ); +}; + const LearningItem = ({ text, subText, diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 0d6b8e7f2..065825b48 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -1,216 +1,28 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; +import { motion } from "framer-motion"; -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - decryptAssymmetric, - encryptAssymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; -import { - useAddIncidentContact, - useAddUserToOrg, - useDeleteIncidentContact, - useDeleteOrgMembership, - useGetOrgIncidentContact, - useGetOrgUsers, - useGetUserWorkspaceMemberships, - useGetUserWsKey, - useRenameOrg, - useUpdateOrgUserRole, - useUploadWsKey -} from "@app/hooks/api"; +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetRoles } from "@app/hooks/api"; -import { - OrgIncidentContactsTable, - OrgMembersTable, - OrgNameChangeSection, - OrgServiceAccountsTable -} from "./components"; +import { OrgMembersTable } from "./components/OrgMembersTable"; +import { OrgRoleTabSection } from "./components/OrgRoleTabSection"; + +enum TabSections { + Member = "members", + Roles = "roles" +} export const MembersPage = () => { - const host = window.location.origin; - const router = useRouter(); - const { action } = router.query; - const { t } = useTranslation(); const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - const { user } = useUser(); - const { subscription } = useSubscription(); - const { createNotification } = useNotificationContext(); const orgId = currentOrg?._id || ""; - const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId); - const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = - useGetUserWorkspaceMemberships(orgId); - const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || ""); - const { data: incidentContact, isLoading: IsIncidentContactLoading } = - useGetOrgIncidentContact(orgId); - - const renameOrg = useRenameOrg(); - const removeUserOrgMembership = useDeleteOrgMembership(); - const addUserToOrg = useAddUserToOrg(); - const updateOrgUserRole = useUpdateOrgUserRole(); - const uploadWsKey = useUploadWsKey(); - const addIncidentContact = useAddIncidentContact(); - const removeIncidentContact = useDeleteIncidentContact(); - - const [completeInviteLink, setcompleteInviteLink] = useState(""); - - const isMoreUsersNotAllowed = subscription?.memberLimit ? (subscription.membersUsed >= subscription.memberLimit) : false; - - const onRenameOrg = async (name: string) => { - if (!currentOrg?._id) return; - - try { - await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name }); - createNotification({ - text: "Successfully renamed organization", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to rename organization", - type: "error" - }); - } - }; - - const onRemoveUserOrgMembership = async (membershipId: string) => { - if (!currentOrg?._id) return; - - try { - 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 onAddUserToOrg = async (email: string) => { - if (!currentOrg?._id) return; - - try { - const { data } = await addUserToOrg.mutateAsync({ - organizationId: currentOrg?._id, - inviteeEmail: email - }); - setcompleteInviteLink(data?.completeInviteLink); - - // 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 onUpdateOrgUserRole = 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 onGrantUserAccess = async (userId: 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, - nonce, - encryptedKey: ciphertext, - workspaceId: currentWorkspace?._id || "" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to grant access to user", - type: "error" - }); - } - }; - - const onAddIncidentContact = async (email: string) => { - if (!currentOrg?._id) return; - - try { - await addIncidentContact.mutateAsync({ orgId, email }); - createNotification({ - text: "Successfully added incident contact", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to add incident contact", - type: "error" - }); - } - }; - - const onRemoveIncidentContact = async (email: string) => { - if (!currentOrg?._id) return; - - try { - await removeIncidentContact.mutateAsync({ orgId, email }); - createNotification({ - text: "Successfully removed incident contact", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove incident contact", - type: "error" - }); - } - }; + const { data: roles } = useGetRoles({ + orgId + }); return (
@@ -218,20 +30,28 @@ export const MembersPage = () => {

{t("section.members.org-members")}

- + + + Members + {process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && ( + Roles + )} + + + + + + + + + +
); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx deleted file mode 100644 index cfafae765..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { - faContactBook, - faMagnifyingGlass, - faPlus, - faTrash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { - Button, - DeleteActionModal, - EmailServiceSetupModal, - EmptyState, - FormControl, - IconButton, - Input, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { usePopUp } from "@app/hooks"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { IncidentContact } from "@app/hooks/api/types"; - -type Props = { - isLoading?: boolean; - contacts?: IncidentContact[]; - onRemoveContact: (email: string) => Promise; - onAddContact: (email: string) => Promise; -}; - -const addContactFormSchema = yup.object({ - email: yup.string().email().required().label("Email").trim() -}); - -type TAddContactForm = yup.InferType; - -export const OrgIncidentContactsTable = ({ - contacts = [], - onAddContact, - onRemoveContact, - isLoading -}: Props) => { - const [searchContact, setSearchContact] = useState(""); - const { data: serverDetails } = useFetchServerStatus(); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addContact", - "removeContact", - "setUpEmail" - ] as const); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: yupResolver(addContactFormSchema) }); - - const onAddIncidentContact = ({ email }: TAddContactForm) => { - onAddContact(email); - handlePopUpClose("addContact"); - reset(); - }; - - const onRemoveIncidentContact = async () => { - const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email; - await onRemoveContact(incidentContactEmail); - handlePopUpClose("removeContact"); - }; - - const filteredContacts = contacts.filter(({ email }) => - email.toLocaleLowerCase().includes(searchContact) - ); - - return ( -
-
-
- setSearchContact(e.target.value)} - leftIcon={} - placeholder="Search incident contact by email..." - /> -
-
- -
-
-
- - - - - - - - - {isLoading && } - {filteredContacts?.map(({ email }) => ( - - - - - ))} - -
Email -
{email} - handlePopUpOpen("removeContact", { email })} - > - - -
- {filteredContacts?.length === 0 && !isLoading && ( - - )} -
-
- { - handlePopUpToggle("addContact", isOpen); - reset(); - }} - > - -
- ( - - - - )} - /> -
- - -
- -
-
- handlePopUpToggle("removeContact", isOpen)} - onDeleteApproved={onRemoveIncidentContact} - /> - handlePopUpToggle("setUpEmail", isOpen)} - /> -
- ); -}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx deleted file mode 100644 index b1df7cd68..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable"; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx index 2955f5b51..e13a2ca0a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useRouter } from "next/router"; import { @@ -14,6 +14,10 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + decryptAssymmetric, + encryptAssymmetric +} from "@app/components/utilities/cryptography/crypto"; import { Button, DeleteActionModal, @@ -37,26 +41,23 @@ import { Tr, UpgradePlanModal } from "@app/components/v2"; -import { useOrganization, useWorkspace } from "@app/context"; +import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; -import { useGetSSOConfig } from "@app/hooks/api"; +import { + useAddUserToOrg, + useDeleteOrgMembership, + useGetOrgUsers, + useGetSSOConfig, + useGetUserWorkspaceMemberships, + useGetUserWsKey, + useUpdateOrgUserRole, + useUploadWsKey +} from "@app/hooks/api"; +import { TRole } from "@app/hooks/api/roles/types"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { OrgUser, Workspace } from "@app/hooks/api/types"; type Props = { - members?: OrgUser[]; - workspaceMemberships?: Record; - orgName: string; - isLoading?: boolean; - isMoreUserNotAllowed: boolean; - onRemoveMember: (userId: string) => Promise; - onInviteMember: (email: string) => Promise; - onRoleChange: (membershipId: string, role: string) => Promise; - onGrantAccess: (userId: string, publicKey: string) => Promise; - // the current user id to block remove org button - userId: string; - completeInviteLink: string | undefined; - setCompleteInviteLink: Dispatch>; + roles?: TRole[]; }; const addMemberFormSchema = yup.object({ @@ -65,27 +66,21 @@ const addMemberFormSchema = yup.object({ type TAddMemberForm = yup.InferType; -export const OrgMembersTable = ({ - members = [], - workspaceMemberships = {}, - orgName, - isMoreUserNotAllowed, - onRemoveMember, - onInviteMember, - onGrantAccess, - onRoleChange, - userId, - isLoading, - completeInviteLink, - setCompleteInviteLink -}: Props) => { +export const OrgMembersTable = ({ roles = [] }: Props) => { const router = useRouter(); const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); - const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? ""); + const { workspaces, currentWorkspace } = useWorkspace(); + const { user } = useUser(); + const userId = user?._id || ""; + const orgId = currentOrg?._id || ""; + const workspaceId = currentWorkspace?._id || ""; + + const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId); const [searchMemberFilter, setSearchMemberFilter] = useState(""); const { data: serverDetails } = useFetchServerStatus(); - const { workspaces } = useWorkspace(); + const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addMember", @@ -93,6 +88,23 @@ export const OrgMembersTable = ({ "upgradePlan", "setUpEmail" ] as const); + const { subscription } = useSubscription(); + + const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); + const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = + useGetUserWorkspaceMemberships(orgId); + const { data: wsKey } = useGetUserWsKey(workspaceId); + + const removeUserOrgMembership = useDeleteOrgMembership(); + const addUserToOrg = useAddUserToOrg(); + const updateOrgUserRole = useUpdateOrgUserRole(); + const uploadWsKey = useUploadWsKey(); + + const [completeInviteLink, setCompleteInviteLink] = useState(""); + + const isMoreUsersNotAllowed = subscription?.memberLimit + ? subscription.membersUsed >= subscription.memberLimit + : false; useEffect(() => { if (router.query.action === "invite") { @@ -108,32 +120,100 @@ export const OrgMembersTable = ({ } = useForm({ resolver: yupResolver(addMemberFormSchema) }); const onAddMember = async ({ email }: TAddMemberForm) => { - await onInviteMember(email); + if (!currentOrg?._id) return; + + try { + const { data } = await addUserToOrg.mutateAsync({ + organizationId: currentOrg?._id, + inviteeEmail: email + }); + setCompleteInviteLink(data?.completeInviteLink); + // 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" + }); + } if (serverDetails?.emailConfigured) { handlePopUpClose("addMember"); } - reset(); }; + const onAddUserToOrg = async (email: string) => { + if (!currentOrg?._id) return; + + try { + const { data } = await addUserToOrg.mutateAsync({ + organizationId: currentOrg?._id, + inviteeEmail: email + }); + setCompleteInviteLink(data?.completeInviteLink); + + // 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 onRemoveOrgMemberApproved = async () => { - const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id; - await onRemoveMember(orgMembershipId); + const membershipId = (popUp?.removeMember?.data as { id: string })?.id; + if (!currentOrg?._id) return; + + try { + 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" + }); + } handlePopUpClose("removeMember"); }; const isIamOwner = useMemo( - () => members.find(({ user }) => userId === user?._id)?.role === "owner", + () => members?.find(({ user: u }) => userId === u?._id)?.role === "owner", [userId, members] ); + const findRoleFromId = useCallback( + (roleId: string) => { + return roles.find(({ _id: id }) => id === roleId); + }, + [roles] + ); + const filterdUser = useMemo( () => - members.filter( - ({ user, inviteEmail }) => - user?.firstName?.toLowerCase().includes(searchMemberFilter) || - user?.lastName?.toLowerCase().includes(searchMemberFilter) || - user?.email?.toLowerCase().includes(searchMemberFilter) || + members?.filter( + ({ user: u, inviteEmail }) => + u?.firstName?.toLowerCase().includes(searchMemberFilter) || + u?.lastName?.toLowerCase().includes(searchMemberFilter) || + u?.email?.toLowerCase().includes(searchMemberFilter) || inviteEmail?.includes(searchMemberFilter) ), [members, searchMemberFilter] @@ -147,11 +227,65 @@ export const OrgMembersTable = ({ return () => clearTimeout(timer); }, [isInviteLinkCopied]); + 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 onGrantAccess = async (grantedUserId: 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: grantedUserId, + nonce, + encryptedKey: ciphertext, + workspaceId: currentWorkspace?._id || "" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to grant access to user", + type: "error" + }); + } + }; + const copyTokenToClipboard = () => { navigator.clipboard.writeText(completeInviteLink as string); setInviteLinkCopied.on(); }; + const isLoading = isMembersLoading || IsWsMembershipLoading; + return (
@@ -175,7 +309,7 @@ export const OrgMembersTable = ({ return; } - if (isMoreUserNotAllowed) { + if (isMoreUsersNotAllowed) { handlePopUpOpen("upgradePlan"); } else { handlePopUpOpen("addMember"); @@ -200,106 +334,116 @@ export const OrgMembersTable = ({ {isLoading && } {!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]; + filterdUser?.map( + ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => { + const name = u ? `${u.firstName} ${u.lastName}` : "-"; + const email = u?.email || inviteEmail; + const userWs = workspaceMemberships?.[u?._id]; - return ( - - {name} - {email} - - {status === "accepted" && ( - - )} - {(status === "invited" || status === "verified") && - serverDetails?.emailConfigured && ( - + )} + {status === "completed" && ( + )} - {status === "completed" && ( - - )} - - - {userWs ? ( - userWs?.map(({ name: wsName, _id }) => ( - - {wsName} - - )) - ) : ( -
- {(status === "invited" || status === "verified") && - serverDetails?.emailConfigured ? ( - - This user hasn't accepted the invite yet + + + {userWs ? ( + userWs?.map(({ name: wsName, _id }) => ( + + {wsName} - ) : ( - - This user isn't part of any projects yet - - )} - {router.query.id !== "undefined" && - !( - (status === "invited" || status === "verified") && - serverDetails?.emailConfigured - ) && ( - + )) + ) : ( +
+ {(status === "invited" || status === "verified") && + serverDetails?.emailConfigured ? ( + + This user hasn't accepted the invite yet + + ) : ( + + This user isn't part of any projects yet + )} -
- )} - - - {userId !== user?._id && ( - handlePopUpOpen("removeMember", { id: orgMembershipId })} - > - - - )} - - - ); - })} + {router.query.id !== "undefined" && + !( + (status === "invited" || status === "verified") && + serverDetails?.emailConfigured + ) && ( + + )} +
+ )} + + + {userId !== u?._id && ( + + handlePopUpOpen("removeMember", { id: orgMembershipId }) + } + > + + + )} + + + ); + } + )} {!isLoading && filterdUser?.length === 0 && ( @@ -315,7 +459,7 @@ export const OrgMembersTable = ({ }} > {!completeInviteLink && ( diff --git a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx deleted file mode 100644 index 525b52471..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { faCheck } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { Button, FormControl, Input } from "@app/components/v2"; - -type Props = { - orgName?: string; - onOrgNameChange: (name: string) => Promise; -}; - -const formSchema = yup.object({ - name: yup.string().required().label("Project Name") -}); - -type FormData = yup.InferType; - -export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => { - const { - handleSubmit, - control, - reset, - formState: { isDirty, isSubmitting } - } = useForm({ resolver: yupResolver(formSchema) }); - const { t } = useTranslation(); - - useEffect(() => { - reset({ name: orgName }); - }, [orgName]); - - const onFormSubmit = async ({ name }: FormData) => { - await onOrgNameChange(name); - }; - - return ( -
-
-

{t("common.display-name")}

-
- ( - - - - )} - control={control} - name="name" - /> -
- -
-
- ); -}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/index.tsx deleted file mode 100644 index 4d86fcddb..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgNameChangeSection } from "./OrgNameChangeSection"; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx new file mode 100644 index 000000000..d761706b4 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx @@ -0,0 +1,167 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { faArrowLeft, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useCreateRole, useUpdateRole } from "@app/hooks/api"; +import { TRole } from "@app/hooks/api/roles/types"; + +import { + formRolePermission2API, + formSchema, + rolePermission2Form, + TFormSchema +} from "./OrgRoleModifySection.utils"; +import { OrgRoleWorkspacePermission } from "./OrgRoleWorkspacePermission"; + +type Props = { + role?: TRole; + onGoBack: VoidFunction; +}; + +export const OrgRoleModifySection = ({ role, onGoBack }: Props) => { + const [searchPermission, setSearchPermission] = useState(""); + + const isNonEditable = ["owner", "admin", "member"].includes(role?.slug || ""); + const isNewRole = !role?.slug; + + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + const { + handleSubmit, + register, + formState: { isSubmitting, isDirty, errors }, + setValue, + control + } = useForm({ + defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, + resolver: yupResolver(formSchema) + }); + + const { mutateAsync: createRole } = useCreateRole(); + const { mutateAsync: updateRole } = useUpdateRole(); + + const handleRoleUpdate = async (el: TFormSchema) => { + if (!role?._id) return; + + try { + await updateRole({ + orgId, + id: role?._id, + ...el, + permissions: formRolePermission2API(el.permissions) + }); + createNotification({ type: "success", text: "Successfully updated role" }); + onGoBack(); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to update role" }); + } + }; + + const handleFormSubmit = async (el: TFormSchema) => { + if (!isNewRole) { + await handleRoleUpdate(el); + return; + } + + try { + await createRole({ + orgId, + ...el, + permissions: formRolePermission2API(el.permissions) + }); + createNotification({ type: "success", text: "Created new role" }); + onGoBack(); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to create role" }); + } + }; + + return ( +
+
+
+

+ {isNewRole ? "New" : "Edit"} Role +

+ +
+

+ Roles are used to grant access to particular resources in your organization +

+
+ + + + + + + + + +
+
+

Add Permission

+
+
+ setSearchPermission(e.target.value)} + leftIcon={} + placeholder="Search permissions..." + /> +
+
+
+ +
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts new file mode 100644 index 000000000..3a2936697 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -0,0 +1,93 @@ +/* eslint-disable no-param-reassign */ +import * as yup from "yup"; + +import { TPermission } from "@app/hooks/api/roles/types"; + +const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const; + +export const formSchema = yup.object({ + name: yup.string().required().label("Name"), + description: yup.string(), + slug: yup.string().required().label("Slug"), + permissions: yup.object({ + workspace: yup.lazy((val) => + yup.object( + Object.fromEntries( + Object.entries(val || {}).map(([k]) => [ + k, + yup.object({ + read: yup.bool(), + edit: yup.bool(), + delete: yup.bool(), + create: yup.bool() + }) + ]) + ) + ) + ) + }) +}); + +export type TFormSchema = yup.InferType; + +const api2FormWorkspace = ( + formVal: TFormSchema["permissions"]["workspace"], + permission: TPermission +) => { + if (permission.subject !== "workspace") return; + const isCustomRule = Boolean(permission?.condition?.id); + if (isCustomRule && !formVal?.custom) { + formVal.custom = { read: true, edit: true, delete: true, create: true }; + } + + const workspaceId = permission?.condition?.id || "all"; + if (!formVal?.[workspaceId]) + formVal[workspaceId] = { read: false, edit: false, create: false, delete: false }; + formVal[workspaceId][permission.action] = true; +}; + +// convert role permission to form compatiable data structure +export const rolePermission2Form = (permissions: TPermission[] = []) => { + const formVal: TFormSchema["permissions"] = { + workspace: {} + }; + + permissions.forEach((permission) => { + api2FormWorkspace(formVal?.workspace, permission); + }); + + return formVal; +}; + +const form2ApiWorkspace = ( + permissions: TPermission[], + workspace: TFormSchema["permissions"]["workspace"] +) => { + const isFullAccess = PERMISSION_ACTIONS.every((action) => workspace?.all?.[action]); + + PERMISSION_ACTIONS.forEach((action) => { + if (workspace?.all?.[action]) permissions.push({ action, subject: "workspace" }); + }); + + if (!isFullAccess) { + Object.keys(workspace) + .filter((id) => id !== "all" && id !== "custom") // remove all and custom for iter + .forEach((workspaceId) => { + const actions = Object.keys(workspace[workspaceId]) as ["read", "edit", "create", "delete"]; + actions.forEach((action) => { + // if not full access for an action + if (!workspace?.all?.[action] && workspace[workspaceId][action]) { + permissions.push({ action, subject: "workspace", condition: { id: workspaceId } }); + } + }); + }); + } +}; + +export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { + const permissions: TPermission[] = []; + // easy deep copy + if (formVal?.workspace) + form2ApiWorkspace(permissions, JSON.parse(JSON.stringify(formVal.workspace))); + return permissions; +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleWorkspacePermission.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleWorkspacePermission.tsx new file mode 100644 index 000000000..deb1bed20 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleWorkspacePermission.tsx @@ -0,0 +1,205 @@ +import { useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faClipboardList } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { motion } from "framer-motion"; + +import { + Checkbox, + Select, + SelectItem, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; + +import { TFormSchema } from "./OrgRoleModifySection.utils"; + +type Props = { + isNonEditable?: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum WorkspacePermission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-acess", + Custom = "custom" +} + +export const OrgRoleWorkspacePermission = ({ isNonEditable, setValue, control }: Props) => { + const { workspaces } = useWorkspace(); + + const customWorkspaceRule = useWatch({ + control, + name: "permissions.workspace.custom" + }); + const isCustom = Boolean(customWorkspaceRule); + const allWorkspaceRule = useWatch({ control, name: "permissions.workspace.all" }); + + const selectedWsTopVal = useMemo(() => { + const { read, delete: del, edit, create } = allWorkspaceRule || {}; + if (read && del && edit && create) return WorkspacePermission.FullAccess; + if (read) return WorkspacePermission.ReadOnly; + return WorkspacePermission.NoAccess; + }, [allWorkspaceRule]); + + const handleTopLevelPermissionChange = (val: WorkspacePermission) => { + switch (val) { + case WorkspacePermission.NoAccess: + setValue("permissions.workspace", {}, { shouldDirty: true }); + break; + case WorkspacePermission.FullAccess: + setValue( + "permissions.workspace", + { all: { read: true, edit: true, create: true, delete: true } }, + { shouldDirty: true } + ); + break; + case WorkspacePermission.ReadOnly: + setValue( + "permissions.workspace", + { all: { read: true, edit: false, create: false, delete: false } }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.workspace", + { custom: { read: false, edit: false, create: false, delete: false } }, + { shouldDirty: true } + ); + break; + } + }; + + return ( +
+
+
+ +
+
+
Projects
+
User project access control
+
+
+ +
+
+ + + + + + + + + + + + + {isCustom && + workspaces?.map(({ name, _id: id }) => ( + + + + + + + + ))} + +
+ ReadCreateEditDelete
{name} + ( +
+ +
+ )} + /> +
+ ( +
+ +
+ )} + /> +
+ ( +
+ +
+ )} + /> +
+ ( +
+ +
+ )} + /> +
+
+
+
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/index.tsx new file mode 100644 index 000000000..86de1647a --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/index.tsx @@ -0,0 +1 @@ +export { OrgRoleModifySection } from "./OrgRoleModifySection"; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx new file mode 100644 index 000000000..631266168 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx @@ -0,0 +1,46 @@ +import { motion } from "framer-motion"; + +import { usePopUp } from "@app/hooks"; + +import { TRole } from "~/hooks/api/roles/types"; + +import { OrgRoleModifySection } from "./OrgRoleModifySection"; +import { OrgRoleTable } from "./OrgRoleTable"; + +type Props = { + roles?: TRole[]; + isRolesLoading?: boolean; +}; + +export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const); + + return popUp.editRole.isOpen ? ( + + handlePopUpClose("editRole")} + /> + + ) : ( + + handlePopUpOpen("editRole", role)} + /> + + ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx new file mode 100644 index 000000000..2bc09cb89 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { + Button, + DeleteActionModal, + IconButton, + Input, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useDeleteRole } from "@app/hooks/api"; +import { TRole } from "@app/hooks/api/roles/types"; + +import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; +import { usePopUp } from "~/hooks/usePopUp"; + +type Props = { + isRolesLoading?: boolean; + roles?: TRole[]; + onSelectRole: (role?: TRole) => void; +}; + +export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props) => { + const [searchRoles, setSearchRoles] = useState(""); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + const { createNotification } = useNotificationContext(); + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); + + const { mutateAsync: deleteRole } = useDeleteRole(); + + const handleRoleDelete = async () => { + const { _id: id } = popUp?.deleteRole?.data as TRole; + try { + await deleteRole({ + orgId, + id + }); + createNotification({ type: "success", text: "Successfully removed the role" }); + handlePopUpClose("deleteRole"); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to create role" }); + } + }; + + return ( +
+
+
+ setSearchRoles(e.target.value)} + leftIcon={} + placeholder="Search roles..." + /> +
+ +
+
+ + + + + + + + + + + {isRolesLoading && } + {roles?.map((role) => { + const { _id: id, name, createdAt, slug } = role; + const isNonMutatable = ["owner", "admin", "member"].includes(slug); + + return ( + + + + + + + ); + })} + +
NameSlugCreated At +
{name}{slug} + {createdAt ? format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa") : "-"} + +
+ + onSelectRole(role)} + variant="plain" + > + + + + + handlePopUpOpen("deleteRole", role)} + variant="plain" + isDisabled={isNonMutatable} + > + + + +
+
+
+
+ handlePopUpClose("deleteRole")} + onDeleteApproved={handleRoleDelete} + /> +
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/index.tsx new file mode 100644 index 000000000..a78c88bcb --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/index.tsx @@ -0,0 +1 @@ +export { OrgRoleTabSection } from "./OrgRoleTabSection"; diff --git a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx deleted file mode 100644 index bd9f601c2..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useRouter } from "next/router"; -import { - faCheck, - faCopy, - faMagnifyingGlass, - faPencil, - faPlus, - faServer, - faTrash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { - Button, - DeleteActionModal, - EmptyState, - FormControl, - IconButton, - Input, - Modal, - ModalContent, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { useOrganization, useWorkspace } from "@app/context"; -import { usePopUp, useToggle } from "@app/hooks"; -import { - useCreateServiceAccount, - useDeleteServiceAccount, - useGetServiceAccounts -} from "@app/hooks/api"; - -const serviceAccountExpiration = [ - { label: "1 Day", value: 86400 }, - { label: "7 Days", value: 604800 }, - { label: "1 Month", value: 2592000 }, - { label: "6 months", value: 15552000 }, - { label: "12 months", value: 31104000 }, - { label: "Never", value: -1 } -]; - -const addServiceAccountFormSchema = yup.object({ - name: yup.string().required().label("Name").trim(), - expiresIn: yup.string().required().label("Service Account Expiration") -}); - -type TAddServiceAccountForm = yup.InferType; - -export const OrgServiceAccountsTable = () => { - const router = useRouter(); - const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - - const orgId = currentOrg?._id || ""; - const [step, setStep] = useState(0); - const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); - const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); - const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); - const [accessKey, setAccessKey] = useState(""); - const [publicKey, setPublicKey] = useState(""); - const [privateKey, setPrivateKey] = useState(""); - const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addServiceAccount", - "removeServiceAccount" - ] as const); - - const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = - useGetServiceAccounts(orgId); - - const createServiceAccount = useCreateServiceAccount(); - const removeServiceAccount = useDeleteServiceAccount(); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isAccessKeyCopied) { - timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); - } - - if (isPublicKeyCopied) { - timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); - } - - if (isPrivateKeyCopied) { - timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) }); - - const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { - if (!currentOrg?._id) return; - - const keyPair = generateKeyPair(); - setPublicKey(keyPair.publicKey); - setPrivateKey(keyPair.privateKey); - - const serviceAccountDetails = await createServiceAccount.mutateAsync({ - name, - organizationId: currentOrg?._id, - publicKey: keyPair.publicKey, - expiresIn: Number(expiresIn) - }); - - setAccessKey(serviceAccountDetails.serviceAccountAccessKey); - - setStep(1); - reset(); - }; - - const onRemoveServiceAccount = async () => { - const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; - await removeServiceAccount.mutateAsync(serviceAccountId); - handlePopUpClose("removeServiceAccount"); - }; - - const filteredServiceAccounts = useMemo( - () => - serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)), - [serviceAccounts, searchServiceAccountFilter] - ); - - const renderStep = (stepToRender: number) => { - switch (stepToRender) { - case 0: - return ( -
- ( - - - - )} - /> - { - return ( - - - - ); - }} - /> -
- - -
- - ); - case 1: - return ( - <> -

Access Key

-
-

{accessKey}

- { - navigator.clipboard.writeText(accessKey); - setIsAccessKeyCopied.on(); - }} - > - - - Copy - - -
-

Public Key

-
-

{publicKey}

- { - navigator.clipboard.writeText(publicKey); - setIsPublicKeyCopied.on(); - }} - > - - - Copy - - -
-

Private Key

-
-

{privateKey}

- { - navigator.clipboard.writeText(privateKey); - setIsPrivateKeyCopied.on(); - }} - > - - - Copy - - -
- - ); - default: - return
; - } - }; - - return ( -
-
-
- setSearchServiceAccountFilter(e.target.value)} - leftIcon={} - placeholder="Search service accounts..." - /> -
- -
- - - - - - - - {isServiceAccountsLoading && ( - - )} - {!isServiceAccountsLoading && - filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => { - return ( - - - - - - ); - })} - -
NameValid Until -
{name}{new Date(expiresAt).toUTCString()} -
- { - if (currentWorkspace?._id) { - router.push( - `/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}` - ); - } - }} - className="mr-2" - > - - - - handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId }) - } - > - - -
-
- {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( - - )} -
- { - handlePopUpToggle("addServiceAccount", isOpen); - reset(); - }} - > - - {renderStep(step)} - - - handlePopUpToggle("removeServiceAccount", isOpen)} - onDeleteApproved={onRemoveServiceAccount} - /> -
- ); -}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/index.tsx deleted file mode 100644 index 8e900f6fa..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/index.tsx b/frontend/src/views/Org/MembersPage/components/index.tsx deleted file mode 100644 index 796bb6aa3..000000000 --- a/frontend/src/views/Org/MembersPage/components/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable"; -export { OrgMembersTable } from "./OrgMembersTable"; -export { OrgNameChangeSection } from "./OrgNameChangeSection"; -export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable"; -