mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
Standardize org members page styling
This commit is contained in:
@@ -5,7 +5,8 @@ import {
|
|||||||
ServiceTokenDataV3,
|
ServiceTokenDataV3,
|
||||||
ServiceTokenDataV3Key, // TODO: remove
|
ServiceTokenDataV3Key, // TODO: remove
|
||||||
Organization,
|
Organization,
|
||||||
ServiceMembershipOrg
|
ServiceMembershipOrg,
|
||||||
|
ServiceMembership
|
||||||
} from "../../../models";
|
} from "../../../models";
|
||||||
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||||
import {
|
import {
|
||||||
@@ -431,6 +432,10 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
serviceTokenData: serviceTokenData._id
|
serviceTokenData: serviceTokenData._id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ServiceMembership.deleteMany({
|
||||||
|
service: serviceTokenData._id,
|
||||||
|
});
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
req.authData,
|
req.authData,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from "./apiKeys";
|
|
||||||
export * from "./admin"
|
export * from "./admin"
|
||||||
|
export * from "./apiKeys";
|
||||||
export * from "./auditLogs";
|
export * from "./auditLogs";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
@@ -16,6 +16,7 @@ export * from "./secretImports";
|
|||||||
export * from "./secretRotation";
|
export * from "./secretRotation";
|
||||||
export * from "./secrets";
|
export * from "./secrets";
|
||||||
export * from "./secretSnapshots";
|
export * from "./secretSnapshots";
|
||||||
|
export * from "./serverDetails";
|
||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
|||||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||||
`/api/v1/organization/${orgId}/workspace-memberships`
|
`/api/v1/organization/${orgId}/workspace-memberships`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -385,6 +386,8 @@ export const useAddServiceToWorkspace = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: update
|
||||||
|
|
||||||
export const useDeleteServiceFromWorkspace = () => {
|
export const useDeleteServiceFromWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
|
|
||||||
import { OrgMembersTable } from "./components/OrgMembersTable";
|
import {
|
||||||
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
|
OrgMembersTab,
|
||||||
import { OrgServiceTokenTab } from "./components/OrgServiceTokenTab";
|
OrgRoleTabSection,
|
||||||
|
OrgServiceTokenTab
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Member = "members",
|
Member = "members",
|
||||||
@@ -25,20 +25,12 @@ export const MembersPage = withPermission(
|
|||||||
</p>
|
</p>
|
||||||
<Tabs defaultValue={TabSections.Member}>
|
<Tabs defaultValue={TabSections.Member}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSections.Member}>Members</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.ServiceTokens}>Service Accounts</Tab>
|
<Tab value={TabSections.ServiceTokens}>Machine Identities</Tab>
|
||||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Member}>
|
<TabPanel value={TabSections.Member}>
|
||||||
<motion.div
|
<OrgMembersTab />
|
||||||
key="panel-1"
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
|
||||||
>
|
|
||||||
<OrgMembersTable />
|
|
||||||
</motion.div>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={TabSections.ServiceTokens}>
|
<TabPanel value={TabSections.ServiceTokens}>
|
||||||
<OrgServiceTokenTab />
|
<OrgServiceTokenTab />
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { OrgMembersSection } from "./components";
|
||||||
|
|
||||||
|
export const OrgMembersTab = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="panel-service-token"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<OrgMembersSection />
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
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,
|
||||||
|
useFetchServerStatus
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const addMemberFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label("Email").trim().lowercase()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["addMember"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
|
||||||
|
completeInviteLink: string;
|
||||||
|
setCompleteInviteLink: (link: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddOrgMemberModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpToggle,
|
||||||
|
completeInviteLink,
|
||||||
|
setCompleteInviteLink
|
||||||
|
}: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
||||||
|
|
||||||
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await addUserMutateAsync({
|
||||||
|
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) {
|
||||||
|
handlePopUpToggle("addMember", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(completeInviteLink as string);
|
||||||
|
setInviteLinkCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
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(onAddMember)}>
|
||||||
|
<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={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("addMember", false)}
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmailServiceSetupModal,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription,
|
||||||
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
useDeleteOrgMembership,
|
||||||
|
useGetSSOConfig,
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||||
|
import { OrgMembersTable } from "./OrgMembersTable";
|
||||||
|
|
||||||
|
export const OrgMembersSection = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?._id ?? "";
|
||||||
|
|
||||||
|
const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
|
||||||
|
|
||||||
|
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"addMember",
|
||||||
|
"removeMember",
|
||||||
|
"upgradePlan",
|
||||||
|
"setUpEmail"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||||
|
|
||||||
|
const isMoreUsersNotAllowed = subscription?.memberLimit
|
||||||
|
? subscription.membersUsed >= subscription.memberLimit
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const handleAddMemberModal = () => {
|
||||||
|
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
||||||
|
createNotification({
|
||||||
|
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMoreUsersNotAllowed) {
|
||||||
|
console.log("no more users allowed");
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
} else {
|
||||||
|
console.log("yes more users allowed");
|
||||||
|
handlePopUpOpen("addMember");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteMutateAsync({
|
||||||
|
orgId,
|
||||||
|
membershipId: orgMembershipId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed user from org",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove user from the organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpClose("removeMember");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex justify-between mb-8">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
|
Members
|
||||||
|
</p>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Create}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handleAddMemberModal()}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<OrgMembersTable
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
setCompleteInviteLink={setCompleteInviteLink}
|
||||||
|
/>
|
||||||
|
<AddOrgMemberModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
completeInviteLink={completeInviteLink}
|
||||||
|
setCompleteInviteLink={setCompleteInviteLink}
|
||||||
|
/>
|
||||||
|
{/* <AddServiceTokenV3Modal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/> */}
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeMember.isOpen}
|
||||||
|
title={`Are you sure want to remove member with email ${
|
||||||
|
(popUp?.removeMember?.data as { email: string })?.email || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveMemberSubmit(
|
||||||
|
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can add more members if you upgrade your Infisical plan."
|
||||||
|
/>
|
||||||
|
<EmailServiceSetupModal
|
||||||
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
import { useCallback,useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faUsers,
|
||||||
|
faXmark
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useUser
|
||||||
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
useAddUserToOrg,
|
||||||
|
useFetchServerStatus,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetRoles,
|
||||||
|
useUpdateOrgUserRole
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeMember"]>,
|
||||||
|
data?: {
|
||||||
|
orgMembershipId?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
setCompleteInviteLink: (link: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrgMembersTable = ({
|
||||||
|
handlePopUpOpen,
|
||||||
|
setCompleteInviteLink
|
||||||
|
}: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { user } = useUser();
|
||||||
|
const userId = user?._id || "";
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
|
||||||
|
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
|
|
||||||
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
|
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
||||||
|
const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();
|
||||||
|
|
||||||
|
const onRoleChange = async (membershipId: string, role: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateUserOrgRole({
|
||||||
|
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 onResendInvite = async (email: string) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const { data } = await addUserMutateAsync({
|
||||||
|
organizationId: orgId,
|
||||||
|
inviteeEmail: email
|
||||||
|
});
|
||||||
|
|
||||||
|
setCompleteInviteLink(data?.completeInviteLink || "");
|
||||||
|
|
||||||
|
if (!data.completeInviteLink) {
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully resent invite to ${email}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to resend invite to ${email}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading;
|
||||||
|
|
||||||
|
const isIamOwner = useMemo(
|
||||||
|
() => 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: u, inviteEmail }) =>
|
||||||
|
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
u?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
|
),
|
||||||
|
[members, searchMemberFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<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 className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||||
|
{!isLoading &&
|
||||||
|
filterdUser?.map(
|
||||||
|
({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => {
|
||||||
|
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
||||||
|
const email = u?.email || inviteEmail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<>
|
||||||
|
{status === "accepted" && (
|
||||||
|
<Select
|
||||||
|
defaultValue={
|
||||||
|
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||||
|
}
|
||||||
|
isDisabled={userId === u?._id || !isAllowed}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(roles || [])
|
||||||
|
.filter(({ slug }) =>
|
||||||
|
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||||
|
)
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{(status === "invited" || status === "verified") &&
|
||||||
|
serverDetails?.emailConfigured && (
|
||||||
|
<Button
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="w-40"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => onResendInvite(email)}
|
||||||
|
>
|
||||||
|
Resend Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== u?._id && (
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
console.log("TODO: delete logic here");
|
||||||
|
handlePopUpOpen("removeMember", { orgMembershipId, email })
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && filterdUser?.length === 0 && (
|
||||||
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersSection } from "./OrgMembersSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersSection } from "./OrgMembersSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersTab } from "./OrgMembersTab";
|
||||||
@@ -1,575 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import {
|
|
||||||
faCheck,
|
|
||||||
faCopy,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faPlus,
|
|
||||||
faTrash,
|
|
||||||
faUsers
|
|
||||||
} 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 { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmailServiceSetupModal,
|
|
||||||
EmptyState,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TableSkeleton,
|
|
||||||
Tag,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tr,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
useOrganization,
|
|
||||||
useSubscription,
|
|
||||||
useUser,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
useAddUserToOrg,
|
|
||||||
useDeleteOrgMembership,
|
|
||||||
useGetOrgUsers,
|
|
||||||
useGetRoles,
|
|
||||||
useGetSSOConfig,
|
|
||||||
useGetUserWorkspaceMemberships,
|
|
||||||
useGetUserWsKey,
|
|
||||||
useUpdateOrgUserRole,
|
|
||||||
useUploadWsKey} from "@app/hooks/api";
|
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
|
||||||
email: yup.string().email().required().label("Email").trim().lowercase()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
|
||||||
|
|
||||||
export const OrgMembersTable = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { workspaces, currentWorkspace } = useWorkspace();
|
|
||||||
const { user } = useUser();
|
|
||||||
const userId = user?._id || "";
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
const workspaceId = currentWorkspace?._id || "";
|
|
||||||
|
|
||||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
|
||||||
|
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addMember",
|
|
||||||
"removeMember",
|
|
||||||
"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<string | undefined>("");
|
|
||||||
|
|
||||||
const isMoreUsersNotAllowed = subscription?.memberLimit
|
|
||||||
? subscription.membersUsed >= subscription.memberLimit
|
|
||||||
: false;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (router.query.action === "invite") {
|
|
||||||
handlePopUpOpen("addMember");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
|
||||||
|
|
||||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
|
||||||
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 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: 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: u, inviteEmail }) =>
|
|
||||||
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
u?.email?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
inviteEmail?.includes(searchMemberFilter)
|
|
||||||
),
|
|
||||||
[members, searchMemberFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
if (isInviteLinkCopied) {
|
|
||||||
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
|
|
||||||
}
|
|
||||||
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 || isRolesLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mb-4 flex">
|
|
||||||
<div className="mr-4 flex-1">
|
|
||||||
<Input
|
|
||||||
value={searchMemberFilter}
|
|
||||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
placeholder="Search members..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
|
||||||
createNotification({
|
|
||||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMoreUsersNotAllowed) {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("addMember");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TableContainer>
|
|
||||||
<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} innerKey="org-members" />}
|
|
||||||
{!isLoading &&
|
|
||||||
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 (
|
|
||||||
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>{email}</Td>
|
|
||||||
<Td>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<>
|
|
||||||
{status === "accepted" && (
|
|
||||||
<Select
|
|
||||||
defaultValue={
|
|
||||||
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
|
||||||
}
|
|
||||||
isDisabled={userId === u?._id || !isAllowed}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={(selectedRole) =>
|
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(roles || [])
|
|
||||||
.filter(({ slug }) =>
|
|
||||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
|
||||||
)
|
|
||||||
.map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
{(status === "invited" || status === "verified") &&
|
|
||||||
serverDetails?.emailConfigured && (
|
|
||||||
<Button
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
className="w-40"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => onAddUserToOrg(email)}
|
|
||||||
>
|
|
||||||
Resend Invite
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{status === "completed" && (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
|
|
||||||
>
|
|
||||||
Grant Access
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{userWs ? (
|
|
||||||
userWs?.map(({ name: wsName, _id }) => (
|
|
||||||
<Tag key={`user-${u._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(`/project/${workspaces[0]?._id}/members`)
|
|
||||||
}
|
|
||||||
className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
||||||
Add to projects
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{userId !== u?._id && (
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
isDisabled={userId === u?._id || !isAllowed}
|
|
||||||
onClick={() =>
|
|
||||||
handlePopUpOpen("removeMember", { id: orgMembershipId })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isLoading && filterdUser?.length === 0 && (
|
|
||||||
<EmptyState title="No project members found" icon={faUsers} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
<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(onAddMember)}>
|
|
||||||
<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={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
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>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
|
||||||
/>
|
|
||||||
<EmailServiceSetupModal
|
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { OrgMembersTable } from "./OrgMembersTable";
|
|
||||||
+12
-5
@@ -154,6 +154,7 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
if (serviceTokenData) {
|
if (serviceTokenData) {
|
||||||
reset({
|
reset({
|
||||||
name: serviceTokenData.name,
|
name: serviceTokenData.name,
|
||||||
|
expiresIn: "",
|
||||||
role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role,
|
role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role,
|
||||||
trustedIps: serviceTokenData.trustedIps.map(({
|
trustedIps: serviceTokenData.trustedIps.map(({
|
||||||
ipAddress,
|
ipAddress,
|
||||||
@@ -169,6 +170,7 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
name: "",
|
name: "",
|
||||||
|
expiresIn: "",
|
||||||
accessTokenTTL: "7200",
|
accessTokenTTL: "7200",
|
||||||
role: roles[0].slug,
|
role: roles[0].slug,
|
||||||
trustedIps: [{
|
trustedIps: [{
|
||||||
@@ -312,6 +314,11 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Advanced}>
|
||||||
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="expiresIn"
|
name="expiresIn"
|
||||||
@@ -338,10 +345,6 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={TabSections.Advanced}>
|
|
||||||
<div>
|
|
||||||
{tokenTrustedIps.map(({ id }, index) => (
|
{tokenTrustedIps.map(({ id }, index) => (
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -456,7 +459,11 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
>
|
>
|
||||||
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("serviceTokenV3", false)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ export const ServiceTokenV3Section = withPermission(
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between mb-8">
|
<div className="flex justify-between mb-8">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
Service Accounts (Beta)
|
Machines
|
||||||
</p>
|
</p>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Create}
|
I={OrgPermissionActions.Create}
|
||||||
|
|||||||
+89
-15
@@ -1,4 +1,5 @@
|
|||||||
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { useCallback } from "react";
|
||||||
|
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
@@ -7,7 +8,8 @@ import { OrgPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Switch,
|
Select,
|
||||||
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
useOrganization} from "@app/context";
|
useOrganization} from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetOrgServiceMemberships,
|
useGetOrgServiceMemberships,
|
||||||
|
useGetRoles,
|
||||||
useUpdateServiceTokenV3} from "@app/hooks/api";
|
useUpdateServiceTokenV3} from "@app/hooks/api";
|
||||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -50,42 +53,82 @@ export const ServiceTokenV3Table = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||||
const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || "");
|
const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || "");
|
||||||
|
|
||||||
const handleToggleServiceTokenDataStatus = async ({
|
const { data: roles } = useGetRoles({
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChangeRole = async ({
|
||||||
serviceTokenDataId,
|
serviceTokenDataId,
|
||||||
isActive
|
role
|
||||||
}: {
|
}: {
|
||||||
serviceTokenDataId: string;
|
serviceTokenDataId: string;
|
||||||
isActive: boolean;
|
role: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
serviceTokenDataId,
|
serviceTokenDataId,
|
||||||
isActive
|
role
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
|
text: "Successfully updated service account role",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
|
text: "Failed to update service account role",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const handleToggleStatus = async ({
|
||||||
|
// serviceTokenDataId,
|
||||||
|
// isActive
|
||||||
|
// }: {
|
||||||
|
// serviceTokenDataId: string;
|
||||||
|
// isActive: boolean;
|
||||||
|
// }) => {
|
||||||
|
// try {
|
||||||
|
// await updateMutateAsync({
|
||||||
|
// serviceTokenDataId,
|
||||||
|
// isActive
|
||||||
|
// });
|
||||||
|
|
||||||
|
// createNotification({
|
||||||
|
// text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
|
||||||
|
// type: "success"
|
||||||
|
// });
|
||||||
|
// } catch (err) {
|
||||||
|
// console.log(err);
|
||||||
|
// createNotification({
|
||||||
|
// text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
|
||||||
|
// type: "error"
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const findRoleFromId = useCallback(
|
||||||
|
(roleId: string) => {
|
||||||
|
return (roles || []).find(({ _id: id }) => id === roleId);
|
||||||
|
},
|
||||||
|
[roles]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Status</Th>
|
{/* <Th>Status</Th> */}
|
||||||
<Th>Role</Th>
|
<Th>Role</Th>
|
||||||
{/* <Th>Trusted IPs</Th> */}
|
{/* <Th>Trusted IPs</Th> */}
|
||||||
{/* <Th>Access Token TTL</Th> */}
|
{/* <Th>Access Token TTL</Th> */}
|
||||||
@@ -103,7 +146,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
service: {
|
service: {
|
||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
isActive,
|
// isActive,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
// createdAt,
|
// createdAt,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
@@ -116,7 +159,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>
|
{/* <Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.ServiceTokens}
|
||||||
@@ -124,7 +167,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
id={`enable-service-token-${_id}`}
|
id={`enable-service-token-${_id}`}
|
||||||
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
|
onCheckedChange={(value) => handleToggleStatus({
|
||||||
serviceTokenDataId: _id,
|
serviceTokenDataId: _id,
|
||||||
isActive: value
|
isActive: value
|
||||||
})}
|
})}
|
||||||
@@ -135,8 +178,39 @@ export const ServiceTokenV3Table = ({
|
|||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
</Td> */}
|
||||||
|
<Td>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.ServiceTokens}
|
||||||
|
>
|
||||||
|
{(isAllowed) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||||
|
}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
handleChangeRole({
|
||||||
|
serviceTokenDataId: _id,
|
||||||
|
role: selectedRole
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(roles || [])
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</OrgPermissionCan>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{customRole?.slug ?? role}</Td>
|
|
||||||
{/* <Td>
|
{/* <Td>
|
||||||
{trustedIps.map(({
|
{trustedIps.map(({
|
||||||
_id: trustedIpId,
|
_id: trustedIpId,
|
||||||
@@ -211,7 +285,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
{!isLoading && data && data?.length === 0 && (
|
{!isLoading && data && data?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={7}>
|
<Td colSpan={7}>
|
||||||
<EmptyState title="No service token v3 on file" icon={faKey} />
|
<EmptyState title="No system users have been created in this organization" icon={faServer} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { OrgMembersTab } from "./OrgMembersTab";
|
||||||
|
export { OrgRoleTabSection } from "./OrgRoleTabSection";
|
||||||
|
export { OrgServiceTokenTab } from "./OrgServiceTokenTab";
|
||||||
+42
-9
@@ -19,12 +19,13 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useAddServiceToWorkspace ,
|
import { useAddServiceToWorkspace ,
|
||||||
useGetOrgServiceMemberships,
|
useGetOrgServiceMemberships,
|
||||||
useGetWorkspaceServiceMemberships
|
useGetRoles,
|
||||||
} from "@app/hooks/api";
|
useGetWorkspaceServiceMemberships} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
serviceTokenDataId: yup.string()
|
serviceTokenDataId: yup.string().required("ST V3 id is required"),
|
||||||
|
role: yup.string().required("ST V3 role is required")
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -38,6 +39,7 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
popUp,
|
popUp,
|
||||||
handlePopUpToggle
|
handlePopUpToggle
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -47,6 +49,11 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
|
|
||||||
const { data: orgServices } = useGetOrgServiceMemberships(orgId);
|
const { data: orgServices } = useGetOrgServiceMemberships(orgId);
|
||||||
const { data: services } = useGetWorkspaceServiceMemberships(workspaceId);
|
const { data: services } = useGetWorkspaceServiceMemberships(workspaceId);
|
||||||
|
const { data: roles } = useGetRoles({
|
||||||
|
orgId,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
const addServiceToWorkspace = useAddServiceToWorkspace();
|
const addServiceToWorkspace = useAddServiceToWorkspace();
|
||||||
|
|
||||||
const filteredOrgServices = useMemo(() => {
|
const filteredOrgServices = useMemo(() => {
|
||||||
@@ -71,18 +78,19 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
serviceTokenDataId
|
serviceTokenDataId,
|
||||||
|
role
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await addServiceToWorkspace.mutateAsync({
|
await addServiceToWorkspace.mutateAsync({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
serviceId: serviceTokenDataId as string,
|
serviceId: serviceTokenDataId as string,
|
||||||
role: "member"
|
role
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
text: "Successfully added service account to project",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +99,7 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
text: "Failed to add service account to project",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,7 +125,6 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
label="Service Account"
|
label="Service Account"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
@@ -134,6 +141,32 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="role"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(roles || []).map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
+1
-2
@@ -62,7 +62,7 @@ export const ServiceTokenV3Section = withProjectPermission(
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between mb-8">
|
<div className="flex justify-between mb-8">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
Service Accounts (Beta)
|
Service Accounts
|
||||||
</p>
|
</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
@@ -86,7 +86,6 @@ export const ServiceTokenV3Section = withProjectPermission(
|
|||||||
/>
|
/>
|
||||||
<AddServiceTokenV3Modal
|
<AddServiceTokenV3Modal
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
|
|||||||
+98
-5
@@ -1,11 +1,15 @@
|
|||||||
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { useCallback } from "react";
|
||||||
|
import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -15,8 +19,15 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
|
import {
|
||||||
import { useGetWorkspaceServiceMemberships } from "@app/hooks/api";
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useOrganization,
|
||||||
|
useWorkspace,
|
||||||
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetRoles,
|
||||||
|
useGetWorkspaceServiceMemberships} from "@app/hooks/api";
|
||||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -38,12 +49,63 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: update roles thing here
|
||||||
export const ServiceTokenV3Table = ({
|
export const ServiceTokenV3Table = ({
|
||||||
handlePopUpOpen
|
handlePopUpOpen
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
|
||||||
const { data, isLoading } = useGetWorkspaceServiceMemberships(currentWorkspace?._id || "");
|
const { data, isLoading } = useGetWorkspaceServiceMemberships(currentWorkspace?._id || "");
|
||||||
|
|
||||||
|
const { data: roles } = useGetRoles({
|
||||||
|
orgId,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChangeRole = async ({
|
||||||
|
serviceTokenDataId,
|
||||||
|
role
|
||||||
|
}: {
|
||||||
|
serviceTokenDataId: string;
|
||||||
|
role: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
console.log("handle project-level role change vals: ", {
|
||||||
|
serviceTokenDataId,
|
||||||
|
role
|
||||||
|
});
|
||||||
|
|
||||||
|
// await updateMutateAsync({
|
||||||
|
// serviceTokenDataId,
|
||||||
|
// role
|
||||||
|
// });
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated service account role",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update service account role",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findRoleFromId = useCallback(
|
||||||
|
(roleId: string) => {
|
||||||
|
return (roles || []).find(({ _id: id }) => id === roleId);
|
||||||
|
},
|
||||||
|
[roles]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -72,7 +134,38 @@ export const ServiceTokenV3Table = ({
|
|||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{customRole?.slug ?? role}</Td>
|
<Td>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.ServiceTokens}
|
||||||
|
>
|
||||||
|
{(isAllowed) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||||
|
}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
handleChangeRole({
|
||||||
|
serviceTokenDataId: _id,
|
||||||
|
role: selectedRole
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(roles || [])
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</Td>
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -105,7 +198,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
{!isLoading && data && data?.length === 0 && (
|
{!isLoading && data && data?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={7}>
|
<Td colSpan={7}>
|
||||||
<EmptyState title="No service token v3 on file" icon={faKey} />
|
<EmptyState title="No service accounts have been added to this project" icon={faServer} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user