import {RefreshCw} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useEffect, useState} from 'react' import {Link} from 'react-router-dom' import { attachFileSystemToFunction, createAccessPoint, createMountTarget, deleteAccessPoint, deleteMountTarget, detachFileSystemFromFunction, getCloudResource, listCloudResources, } from '@/api/cloudProxyClient' import {cloudQueryKeys} from '@/api/queries/cloudQueries' import {useAccountId} from '@/lib/accountStore' import type {CloudResource} from '@/types/resource' type Props = { resource?: CloudResource runtimeReachable: boolean } type MountTarget = {id: string; subnetId: string | null; availabilityZone: string | null; ipAddress: string | null; state: string | null} type AccessPoint = {id: string; arn: string | null; name: string | null; path: string; uid: number | null; gid: number | null; permissions: string | null; state: string | null} type MountedFunction = {name: string; accessPointId: string | null; mountPath: string | null} const EMPTY_ACCESS_POINT = {name: '', path: '/', uid: '1000', gid: '1000', permissions: '755'} export function FileSystemPanel({resource, runtimeReachable}: Props) { const accountId = useAccountId() const queryClient = useQueryClient() const [subnetId, setSubnetId] = useState('') const [accessPointForm, setAccessPointForm] = useState(EMPTY_ACCESS_POINT) const [attachForm, setAttachForm] = useState({accessPointId: '', functionName: '', mountPath: '/mnt/data'}) const [notice, setNotice] = useState('') const detailKey = ['efs-file-system', accountId, resource?.id] const detailQuery = useQuery({ queryKey: detailKey, queryFn: ({signal}) => getCloudResource('aws', 'filesystems', resource?.id ?? '', signal), enabled: Boolean(resource) && runtimeReachable, }) const functionsQuery = useQuery({ queryKey: cloudQueryKeys.resources('aws', 'serverless'), queryFn: ({signal}) => listCloudResources('aws', 'serverless', undefined, signal), enabled: Boolean(resource) && runtimeReachable, }) useEffect(() => { setNotice('') setSubnetId('') setAccessPointForm(EMPTY_ACCESS_POINT) setAttachForm({accessPointId: '', functionName: '', mountPath: '/mnt/data'}) }, [resource?.id]) const settle = (updated: CloudResource, message: string) => { queryClient.setQueryData(detailKey, updated) setNotice(message) return queryClient.invalidateQueries({queryKey: cloudQueryKeys.resources('aws', 'filesystems')}) } const id = resource?.id ?? '' const addMountTarget = useMutation({ mutationFn: () => createMountTarget('aws', id, subnetId.trim()), onSuccess: (updated) => { setSubnetId(''); return settle(updated, `Added a mount target in ${subnetId.trim()}.`) }, }) const removeMountTarget = useMutation({ mutationFn: (mountTargetId: string) => deleteMountTarget('aws', id, mountTargetId), onSuccess: (updated, mountTargetId) => settle(updated, `Removed mount target ${mountTargetId}.`), }) const addAccessPoint = useMutation({ mutationFn: () => createAccessPoint('aws', id, { ...(accessPointForm.name.trim() ? {name: accessPointForm.name.trim()} : {}), path: accessPointForm.path.trim(), uid: Number(accessPointForm.uid), gid: Number(accessPointForm.gid), permissions: accessPointForm.permissions.trim(), }), onSuccess: (updated) => { setAccessPointForm(EMPTY_ACCESS_POINT); return settle(updated, `Created an access point at ${accessPointForm.path.trim()}.`) }, }) const removeAccessPoint = useMutation({ mutationFn: (accessPointId: string) => deleteAccessPoint('aws', id, accessPointId), onSuccess: (updated, accessPointId) => settle(updated, `Deleted access point ${accessPointId}.`), }) const attach = useMutation({ mutationFn: () => attachFileSystemToFunction('aws', id, {...attachForm, mountPath: attachForm.mountPath.trim()}), onSuccess: (updated) => settle(updated, `${attachForm.functionName} now sees this file system at ${attachForm.mountPath.trim()}.`), }) const detach = useMutation({ mutationFn: (functionName: string) => detachFileSystemFromFunction('aws', id, functionName), onSuccess: (updated, functionName) => settle(updated, `Detached ${functionName}.`), }) if (!resource) { return

Select a file system

Choose a file system to manage its mount targets and access points, and attach it to Lambda functions.

} const fileSystem = detailQuery.data ?? resource const metadata = fileSystem.metadata const mountTargets = Array.isArray(metadata.mountTargets) ? metadata.mountTargets as MountTarget[] : [] const accessPoints = Array.isArray(metadata.accessPoints) ? metadata.accessPoints as AccessPoint[] : [] const mounted = Array.isArray(metadata.functions) ? metadata.functions as MountedFunction[] : [] const functionNames = (functionsQuery.data ?? []).map((fn) => fn.name).sort() const mutations = [addMountTarget, removeMountTarget, addAccessPoint, removeAccessPoint, attach, detach] const disabled = !runtimeReachable || mutations.some((mutation) => mutation.isPending) const error = detailQuery.error ?? mutations.find((mutation) => mutation.error)?.error const accessPointLabel = (accessPointId: string | null) => { const accessPoint = accessPoints.find((candidate) => candidate.id === accessPointId) return accessPoint ? `${accessPoint.name ?? accessPoint.id} (${accessPoint.path})` : accessPointId ?? '—' } const run = (action: () => void) => { setNotice(''); action() } return

EFS file system

{String(metadata.fileSystemId ?? fileSystem.id)} · {String(metadata.throughputMode ?? '')} · {metadata.encrypted ? 'encrypted' : 'not encrypted'}

Mount targets

Functions and tasks reach the file system through a mount target in their subnet.
{mountTargets.length === 0 ?

No mount targets yet. Add one before attaching the file system to a function.

:
{mountTargets.map((mountTarget) => ( ))}
Mount targetSubnetZoneIP addressState
{mountTarget.id} {mountTarget.subnetId ?? '—'} {mountTarget.availabilityZone ?? '—'} {mountTarget.ipAddress ?? '—'} {mountTarget.state ?? '—'}
}
{ event.preventDefault(); run(() => addMountTarget.mutate()) }}>

Access points

An entry point into one directory, as a fixed POSIX user. Lambda mounts a file system through an access point.
{accessPoints.length === 0 ?

No access points yet.

:
{accessPoints.map((accessPoint) => ( ))}
Access pointRoot directoryPOSIX userState
{accessPoint.name && <>{accessPoint.name}
}{accessPoint.id}
{accessPoint.path}{accessPoint.permissions ? ({accessPoint.permissions}) : null} {accessPoint.uid ?? '—'}:{accessPoint.gid ?? '—'} {accessPoint.state ?? '—'}
}
{ event.preventDefault(); run(() => addAccessPoint.mutate()) }}>

Lambda functions

Functions that mount this file system share its files. A function can mount one file system.
{mounted.length === 0 ?

No function mounts this file system.

:
{mounted.map((fn) => ( ))}
FunctionMount pathAccess point
{fn.name} {fn.mountPath ?? '—'} {accessPointLabel(fn.accessPointId)}
} {accessPoints.length > 0 &&
{ event.preventDefault(); run(() => attach.mutate()) }}>
} {accessPoints.length > 0 &&

Lambda needs a VPC to mount a file system. A function not yet in one is put in this file system's mount target subnets with the default security group.

} {notice &&

{notice}

} {error instanceof Error &&

{error.message}

}
}