import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import ms from "ms"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, FormLabel, IconButton, Input, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useCreateSshHost, useGetSshHostById, useGetWorkspaceUsers, useListWorkspaceSshHosts, useUpdateSshHost } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { popUp: UsePopUpState<["sshHost"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshHost"]>, state?: boolean) => void; }; const schema = z .object({ hostname: z.string(), userCertTtl: z .string() .trim() .refine( (val) => ms(val) > 0, "TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..." ) .default("8h"), loginMappings: z .object({ loginUser: z.string().trim().min(1), allowedPrincipals: z.array(z.string().trim()).default([]) }) .array() .default([]) }) .required(); export type FormData = z.infer; export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id); const { data: members = [] } = useGetWorkspaceUsers(projectId); const [expandedMappings, setExpandedMappings] = useState>({}); const { data: sshHost } = useGetSshHostById( (popUp?.sshHost?.data as { sshHostId: string })?.sshHostId || "" ); const { mutateAsync: createMutateAsync } = useCreateSshHost(); const { mutateAsync: updateMutateAsync } = useUpdateSshHost(); const { control, handleSubmit, reset, getValues, setValue, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { hostname: "", userCertTtl: "8h", loginMappings: [] } }); const loginMappingsFormFields = useFieldArray({ control, name: "loginMappings" }); useEffect(() => { if (sshHost) { reset({ hostname: sshHost.hostname, userCertTtl: sshHost.userCertTtl, loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, allowedPrincipals: allowedPrincipals.usernames })) }); setExpandedMappings( Object.fromEntries(sshHost.loginMappings.map((_, index) => [index, false])) ); } else { reset({ hostname: "", userCertTtl: "8h", loginMappings: [] }); } }, [sshHost]); const onFormSubmit = async ({ hostname, userCertTtl, loginMappings }: FormData) => { try { if (!projectId) return; // check if there is already a different host with the same hostname const existingHostnames = sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; if (existingHostnames.includes(hostname)) { createNotification({ text: "A host with this hostname already exists.", type: "error" }); return; } if (sshHost) { await updateMutateAsync({ sshHostId: sshHost.id, hostname, userCertTtl, loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, allowedPrincipals: { usernames: allowedPrincipals } })) }); } else { await createMutateAsync({ projectId, hostname, userCertTtl, loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, allowedPrincipals: { usernames: allowedPrincipals } })) }); } reset(); handlePopUpToggle("sshHost", false); createNotification({ text: `Successfully ${sshHost ? "updated" : "added"} SSH host`, type: "success" }); } catch (err) { console.error(err); createNotification({ text: `Failed to ${sshHost ? "update" : "add"} SSH host`, type: "error" }); } }; const toggleMapping = (index: number) => { setExpandedMappings((prev) => ({ ...prev, [index]: !prev[index] })); }; return ( { reset(); handlePopUpToggle("sshHost", isOpen); }} >
{sshHost && ( )} ( )} /> ( )} />
{loginMappingsFormFields.fields.map(({ id: metadataFieldId }, i) => (
loginMappingsFormFields.remove(i)} >
{expandedMappings[i] && ( <>
Login User ( { const newValue = e.target.value; const loginMappings = getValues("loginMappings"); const isDuplicate = loginMappings.some( (mapping, index) => index !== i && mapping.loginUser === newValue ); if (isDuplicate) { createNotification({ text: "This login user already exists", type: "error" }); return; } field.onChange(e); }} /> )} />
(
{(value.length === 0 ? [""] : value).map( (principal: string, principalIndex: number) => (
{ const newPrincipals = value.filter( (_, idx) => idx !== principalIndex ); onChange(newPrincipals); }} >
) )} {error && {error.message}}
)} />
)}
))}
); };