Update ST V3 to machine identity

This commit is contained in:
Tuan Dang
2023-11-24 19:19:31 +07:00
parent 41c41a647f
commit 595a26a366
71 changed files with 651 additions and 697 deletions
@@ -0,0 +1,19 @@
import { motion } from "framer-motion";
import {
MachineIdentitySection
} from "./components";
export const MachineIdentityTab = () => {
return (
<motion.div
key="panel-machine-identity"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<MachineIdentitySection />
</motion.div>
);
}
@@ -0,0 +1,198 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
} from "@app/components/v2";
import {
useOrganization,
useWorkspace
} from "@app/context";
import {
useAddMachineToWorkspace,
useGetMachineMembershipOrgs,
useGetRoles,
useGetWorkspaceMachineMemberships
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
machineId: 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>;
type Props = {
popUp: UsePopUpState<["machineIdentity"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["machineIdentity"]>, state?: boolean) => void;
};
export const AddMachineIdentityModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || "";
const { data: machineMembershipOrgs } = useGetMachineMembershipOrgs(orgId);
const { data: machineMemberships } = useGetWorkspaceMachineMemberships(workspaceId);
const { data: roles } = useGetRoles({
orgId,
workspaceId
});
const { mutateAsync: addMachineToWorkspaceMutateAsync } = useAddMachineToWorkspace();
const filteredMachineMembershipOrgs = useMemo(() => {
const wsMachineIds = new Map();
machineMemberships?.forEach((machineMembership) => {
wsMachineIds.set(machineMembership.machineIdentity._id, true);
});
return (machineMembershipOrgs || []).filter(
({ machineIdentity: mi }) => !wsMachineIds.has(mi._id)
);
}, [machineMembershipOrgs, machineMemberships]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const onFormSubmit = async ({
machineId,
role
}: FormData) => {
try {
await addMachineToWorkspaceMutateAsync({
workspaceId,
machineId,
role
});
createNotification({
text: "Successfully added machine identity to project",
type: "success"
});
reset();
handlePopUpToggle("machineIdentity", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add machine identity to project",
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.machineIdentity?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("machineIdentity", isOpen);
reset();
}}
>
<ModalContent title="Add Machine Identity to Project">
{filteredMachineMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="machineId"
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Service Account"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{filteredMachineMembershipOrgs.map(({ machineIdentity }) => (
<SelectItem value={machineIdentity._id} key={`org-service-${machineIdentity._id}`}>
{machineIdentity.name}
</SelectItem>
))}
</Select>
</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"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.machineIdentity?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div>All the machine identities in your organization are already added.</div>
<Link href={`/org/${currentWorkspace?.organization}/members`}>
<Button variant="outline_bg">Create a new/another machine identities</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,108 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useWorkspace} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useDeleteMachineFromWorkspace
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddMachineIdentityModal } from "./AddMachineIdentityModal";
import { MachineIdentityTable } from "./MachineIdentityTable";
export const MachineIdentitySection = withProjectPermission(
() => {
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id ?? "";
const { mutateAsync: deleteMutateAsync } = useDeleteMachineFromWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"machineIdentity",
"deleteMachineIdentity",
"upgradePlan"
] as const);
const onRemoveServiceTokenDataSubmit = async (machineId: string) => {
try {
await deleteMutateAsync({
machineId,
workspaceId
});
createNotification({
text: "Successfully removed service account from project",
type: "success"
});
handlePopUpClose("deleteMachineIdentity");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete service account from project",
type: "error"
});
}
}
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">
Machine Identities (MIs)
</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("machineIdentity")}
isDisabled={!isAllowed}
>
Add machine identity
</Button>
)}
</ProjectPermissionCan>
</div>
<MachineIdentityTable
handlePopUpOpen={handlePopUpOpen}
/>
<AddMachineIdentityModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteMachineIdentity.isOpen}
title={`Are you sure want to remove ${
(popUp?.deleteMachineIdentity?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveServiceTokenDataSubmit(
(popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId
)
}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
);
@@ -0,0 +1,211 @@
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,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace,
} from "@app/context";
import {
useGetRoles,
useGetWorkspaceMachineMemberships
} from "@app/hooks/api";
import { MachineTrustedIp} from "@app/hooks/api/machineIdentities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteMachineIdentity", "machineIdentity"]>,
data?: {
machineId?: string;
name?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
trustedIps?: MachineTrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
}
) => void;
};
export const MachineIdentityTable = ({
handlePopUpOpen
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || "";
const { data, isLoading } = useGetWorkspaceMachineMemberships(currentWorkspace?._id || "");
const { data: roles } = useGetRoles({
orgId,
workspaceId
});
const handleChangeRole = async ({
machineId,
role
}: {
machineId: string;
role: string;
}) => {
try {
console.log("handle project-level role change vals: ", {
machineId,
role
});
// TODO: change role
// await updateMutateAsync({
// serviceTokenDataId,
// role
// });
createNotification({
text: "Successfully updated machine identity role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update machine identity role",
type: "error"
});
}
}
const findRoleFromId = useCallback(
(roleId: string) => {
return (roles || []).find(({ _id: id }) => id === roleId);
},
[roles]
);
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Added on</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={7} innerKey="project-machine-identities" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
machineIdentity: {
_id,
name
},
role,
customRole,
createdAt
}) => {
return (
<Tr className="h-10" key={`st-v3-${_id}`}>
<Td>{name}</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({
machineId: _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
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteMachineIdentity", {
machineId: _id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No machine identities have been added to this project" icon={faServer} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -0,0 +1 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -0,0 +1 @@
export { MachineIdentityTab } from "./MachineIdentityTab";