Standardize org members page styling

This commit is contained in:
Tuan Dang
2023-11-24 14:09:56 +07:00
parent c3d2b7d3fc
commit 41c41a647f
20 changed files with 884 additions and 641 deletions

View File

@@ -5,7 +5,8 @@ import {
ServiceTokenDataV3,
ServiceTokenDataV3Key, // TODO: remove
Organization,
ServiceMembershipOrg
ServiceMembershipOrg,
ServiceMembership
} from "../../../models";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
import {
@@ -426,11 +427,15 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
if (!serviceMembershipOrg) throw BadRequestError({
message: "Failed to delete service token"
});
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
await ServiceMembership.deleteMany({
service: serviceTokenData._id,
});
await EEAuditLogService.createAuditLog(
req.authData,
{

View File

@@ -1,5 +1,5 @@
export * from "./apiKeys";
export * from "./admin"
export * from "./apiKeys";
export * from "./auditLogs";
export * from "./auth";
export * from "./bots";
@@ -16,6 +16,7 @@ export * from "./secretImports";
export * from "./secretRotation";
export * from "./secrets";
export * from "./secretSnapshots";
export * from "./serverDetails";
export * from "./serviceTokens";
export * from "./ssoConfig";
export * from "./subscriptions";

View File

@@ -98,6 +98,7 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
`/api/v1/organization/${orgId}/workspace-memberships`
);
return data;
};
@@ -385,6 +386,8 @@ export const useAddServiceToWorkspace = () => {
});
};
// TODO: update
export const useDeleteServiceFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({

View File

@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgMembersTable } from "./components/OrgMembersTable";
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
import { OrgServiceTokenTab } from "./components/OrgServiceTokenTab";
import {
OrgMembersTab,
OrgRoleTabSection,
OrgServiceTokenTab
} from "./components";
enum TabSections {
Member = "members",
@@ -25,20 +25,12 @@ export const MembersPage = withPermission(
</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>Members</Tab>
<Tab value={TabSections.ServiceTokens}>Service Accounts</Tab>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.ServiceTokens}>Machine Identities</Tab>
<Tab value={TabSections.Roles}>Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div
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>
<OrgMembersTab />
</TabPanel>
<TabPanel value={TabSections.ServiceTokens}>
<OrgServiceTokenTab />

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { OrgMembersSection } from "./OrgMembersSection";

View File

@@ -0,0 +1 @@
export { OrgMembersSection } from "./OrgMembersSection";

View File

@@ -0,0 +1 @@
export { OrgMembersTab } from "./OrgMembersTab";

View File

@@ -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&apos;t accepted the invite yet
</Tag>
) : (
<Tag colorSchema="red">
This user isn&apos;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>
);
};

View File

@@ -1 +0,0 @@
export { OrgMembersTable } from "./OrgMembersTable";

View File

@@ -154,6 +154,7 @@ export const AddServiceTokenV3Modal = ({
if (serviceTokenData) {
reset({
name: serviceTokenData.name,
expiresIn: "",
role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role,
trustedIps: serviceTokenData.trustedIps.map(({
ipAddress,
@@ -169,6 +170,7 @@ export const AddServiceTokenV3Modal = ({
} else {
reset({
name: "",
expiresIn: "",
accessTokenTTL: "7200",
role: roles[0].slug,
trustedIps: [{
@@ -312,7 +314,12 @@ export const AddServiceTokenV3Modal = ({
</FormControl>
)}
/>
<Controller
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Advanced}>
<div>
<Controller
control={control}
name="expiresIn"
defaultValue=""
@@ -338,10 +345,6 @@ export const AddServiceTokenV3Modal = ({
</FormControl>
)}
/>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Advanced}>
<div>
{tokenTrustedIps.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
@@ -456,7 +459,11 @@ export const AddServiceTokenV3Modal = ({
>
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("serviceTokenV3", false)}
>
Cancel
</Button>
</div>

View File

@@ -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="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Service Accounts (Beta)
Machines
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}

View File

@@ -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 { format } from "date-fns";
@@ -7,7 +8,8 @@ import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Switch,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
@@ -23,6 +25,7 @@ import {
useOrganization} from "@app/context";
import {
useGetOrgServiceMemberships,
useGetRoles,
useUpdateServiceTokenV3} from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -50,34 +53,74 @@ export const ServiceTokenV3Table = ({
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || "");
const handleToggleServiceTokenDataStatus = async ({
const { data: roles } = useGetRoles({
orgId
});
const handleChangeRole = async ({
serviceTokenDataId,
isActive
role
}: {
serviceTokenDataId: string;
isActive: boolean;
role: string;
}) => {
try {
await updateMutateAsync({
serviceTokenDataId,
isActive
role
});
createNotification({
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
text: "Successfully updated service account role",
type: "success"
});
} catch (err) {
console.log(err);
createNotification({
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
type: "error"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update service account role",
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 (
<TableContainer>
@@ -85,7 +128,7 @@ export const ServiceTokenV3Table = ({
<THead>
<Tr>
<Th>Name</Th>
<Th>Status</Th>
{/* <Th>Status</Th> */}
<Th>Role</Th>
{/* <Th>Trusted IPs</Th> */}
{/* <Th>Access Token TTL</Th> */}
@@ -103,7 +146,7 @@ export const ServiceTokenV3Table = ({
service: {
_id,
name,
isActive,
// isActive,
trustedIps,
// createdAt,
expiresAt,
@@ -116,7 +159,7 @@ export const ServiceTokenV3Table = ({
return (
<Tr className="h-10" key={`st-v3-${_id}`}>
<Td>{name}</Td>
<Td>
{/* <Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
@@ -124,7 +167,7 @@ export const ServiceTokenV3Table = ({
{(isAllowed) => (
<Switch
id={`enable-service-token-${_id}`}
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
onCheckedChange={(value) => handleToggleStatus({
serviceTokenDataId: _id,
isActive: value
})}
@@ -135,8 +178,39 @@ export const ServiceTokenV3Table = ({
</Switch>
)}
</OrgPermissionCan>
</Td>
<Td>{customRole?.slug ?? role}</Td>
</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>
{trustedIps.map(({
_id: trustedIpId,
@@ -211,7 +285,7 @@ export const ServiceTokenV3Table = ({
{!isLoading && data && data?.length === 0 && (
<Tr>
<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>
</Tr>
)}

View File

@@ -0,0 +1,3 @@
export { OrgMembersTab } from "./OrgMembersTab";
export { OrgRoleTabSection } from "./OrgRoleTabSection";
export { OrgServiceTokenTab } from "./OrgServiceTokenTab";

View File

@@ -19,12 +19,13 @@ import {
} from "@app/context";
import { useAddServiceToWorkspace ,
useGetOrgServiceMemberships,
useGetWorkspaceServiceMemberships
} from "@app/hooks/api";
useGetRoles,
useGetWorkspaceServiceMemberships} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
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();
export type FormData = yup.InferType<typeof schema>;
@@ -38,6 +39,7 @@ export const AddServiceTokenV3Modal = ({
popUp,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
@@ -47,6 +49,11 @@ export const AddServiceTokenV3Modal = ({
const { data: orgServices } = useGetOrgServiceMemberships(orgId);
const { data: services } = useGetWorkspaceServiceMemberships(workspaceId);
const { data: roles } = useGetRoles({
orgId,
workspaceId
});
const addServiceToWorkspace = useAddServiceToWorkspace();
const filteredOrgServices = useMemo(() => {
@@ -71,18 +78,19 @@ export const AddServiceTokenV3Modal = ({
});
const onFormSubmit = async ({
serviceTokenDataId
serviceTokenDataId,
role
}: FormData) => {
try {
await addServiceToWorkspace.mutateAsync({
workspaceId,
serviceId: serviceTokenDataId as string,
role: "member"
role
});
createNotification({
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
text: "Successfully added service account to project",
type: "success"
});
@@ -91,7 +99,7 @@ export const AddServiceTokenV3Modal = ({
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
text: "Failed to add service account to project",
type: "error"
});
}
@@ -117,7 +125,6 @@ export const AddServiceTokenV3Modal = ({
label="Service Account"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
@@ -134,6 +141,32 @@ export const AddServiceTokenV3Modal = ({
</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">
<Button
className="mr-4"

View File

@@ -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="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Service Accounts (Beta)
Service Accounts
</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
@@ -86,7 +86,6 @@ export const ServiceTokenV3Section = withProjectPermission(
/>
<AddServiceTokenV3Modal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal

View File

@@ -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 { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
@@ -15,8 +19,15 @@ import {
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
import { useGetWorkspaceServiceMemberships } from "@app/hooks/api";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace,
} from "@app/context";
import {
useGetRoles,
useGetWorkspaceServiceMemberships} from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -36,13 +47,64 @@ type Props = {
isRefreshTokenRotationEnabled?: boolean;
}
) => void;
};
};
// TODO: update roles thing here
export const ServiceTokenV3Table = ({
handlePopUpOpen
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const workspaceId = 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 (
<TableContainer>
@@ -72,7 +134,38 @@ export const ServiceTokenV3Table = ({
return (
<Tr className="h-10" key={`st-v3-${_id}`}>
<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 className="flex justify-end">
<ProjectPermissionCan
@@ -105,7 +198,7 @@ export const ServiceTokenV3Table = ({
{!isLoading && data && data?.length === 0 && (
<Tr>
<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>
</Tr>
)}