the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add skeleton loaders to settings lists

Render skeleton row placeholders while list data is loading for Files,
Secrets, Variables, and Volumes pages. Add react-content-loader based
row
components and use the queries' isLoading flag to show SKELETON_ROWS.

+180 -50
+45 -12
apps/web/src/pages/settings/files/Files.tsx
··· 1 + import ContentLoader from "react-content-loader"; 1 2 import { useRouterState } from "@tanstack/react-router"; 2 3 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 4 import Main from "../../../layouts/Main"; ··· 9 10 import Pagination from "../../../components/pagination"; 10 11 11 12 const PAGE_SIZE = 12; 13 + const SKELETON_ROWS = 8; 14 + 15 + const FileRowSkeleton = ({ index }: { index: number }) => ( 16 + <ContentLoader 17 + speed={1.5} 18 + width="100%" 19 + height={48} 20 + backgroundColor="oklch(var(--b2))" 21 + foregroundColor="oklch(var(--b3))" 22 + style={{ width: "100%" }} 23 + uniqueKey={`file-row-skeleton-${index}`} 24 + > 25 + {/* Path column - wide */} 26 + <rect x="16" y="16" rx="6" ry="6" width="38%" height="16" /> 27 + {/* Created At column */} 28 + <rect x="50%" y="16" rx="6" ry="6" width="22%" height="16" /> 29 + {/* Action column */} 30 + <rect x="88%" y="12" rx="6" ry="6" width="8%" height="24" /> 31 + </ContentLoader> 32 + ); 12 33 13 34 function Files() { 14 35 const [isOpen, setIsOpen] = useState(false); ··· 20 41 const { data } = useSandboxQuery( 21 42 `at:/${pathname.replace("/files", "").replace("sandbox", "io.pocketenv.sandbox")}`, 22 43 ); 23 - const { data: files } = useFilesQuery(data?.sandbox?.id, offset, PAGE_SIZE); 44 + const { data: files, isLoading } = useFilesQuery( 45 + data?.sandbox?.id, 46 + offset, 47 + PAGE_SIZE, 48 + ); 24 49 25 50 const totalPages = files?.total ? Math.ceil(files.total / PAGE_SIZE) : 1; 26 51 ··· 62 87 </tr> 63 88 </thead> 64 89 <tbody> 65 - {files?.files?.map((file) => ( 66 - <tr key={file.id}> 67 - <td className="normal-case text-[14px] font-medium"> 68 - {file.path} 69 - </td> 70 - <td className="normal-case text-[14px] font-medium"> 71 - {dayjs(file.createdAt).format("M/D/YYYY, h:mm:ss A")} 72 - </td> 73 - <td className="normal-case text-[14px]"></td> 74 - </tr> 75 - ))} 90 + {isLoading 91 + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( 92 + <tr key={`skeleton-${i}`}> 93 + <td colSpan={3} className="p-0"> 94 + <FileRowSkeleton index={i} /> 95 + </td> 96 + </tr> 97 + )) 98 + : files?.files?.map((file) => ( 99 + <tr key={file.id}> 100 + <td className="normal-case text-[14px] font-medium"> 101 + {file.path} 102 + </td> 103 + <td className="normal-case text-[14px] font-medium"> 104 + {dayjs(file.createdAt).format("M/D/YYYY, h:mm:ss A")} 105 + </td> 106 + <td className="normal-case text-[14px]"></td> 107 + </tr> 108 + ))} 76 109 </tbody> 77 110 </table> 78 111 </div>
+43 -12
apps/web/src/pages/settings/secrets/Secrets.tsx
··· 1 + import ContentLoader from "react-content-loader"; 1 2 import { useRouterState } from "@tanstack/react-router"; 2 3 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 4 import Main from "../../../layouts/Main"; ··· 9 10 import Pagination from "../../../components/pagination"; 10 11 11 12 const PAGE_SIZE = 12; 13 + const SKELETON_ROWS = 8; 14 + 15 + const SecretRowSkeleton = ({ index }: { index: number }) => ( 16 + <ContentLoader 17 + speed={1.5} 18 + width="100%" 19 + height={48} 20 + backgroundColor="oklch(var(--b2))" 21 + foregroundColor="oklch(var(--b3))" 22 + style={{ width: "100%" }} 23 + uniqueKey={`secret-row-skeleton-${index}`} 24 + > 25 + {/* Name column - wide */} 26 + <rect x="16" y="16" rx="6" ry="6" width="38%" height="16" /> 27 + {/* Created At column */} 28 + <rect x="50%" y="16" rx="6" ry="6" width="22%" height="16" /> 29 + {/* Action column */} 30 + <rect x="88%" y="12" rx="6" ry="6" width="8%" height="24" /> 31 + </ContentLoader> 32 + ); 12 33 13 34 function Secrets() { 14 35 const [isOpen, setIsOpen] = useState(false); ··· 20 41 const { data } = useSandboxQuery( 21 42 `at:/${pathname.replace("/secrets", "").replace("sandbox", "io.pocketenv.sandbox")}`, 22 43 ); 23 - const { data: secrets } = useSecretsQuery( 44 + const { data: secrets, isLoading } = useSecretsQuery( 24 45 data?.sandbox?.id, 25 46 offset, 26 47 PAGE_SIZE, ··· 66 87 </tr> 67 88 </thead> 68 89 <tbody> 69 - {secrets?.secrets?.map((secret) => ( 70 - <tr key={secret.id}> 71 - <td className="normal-case text-[14px] font-medium"> 72 - {secret.name} 73 - </td> 74 - <td className="normal-case text-[14px] font-medium"> 75 - {dayjs(secret.createdAt).format("M/D/YYYY, h:mm:ss A")} 76 - </td> 77 - <td className="normal-case text-[14px]"></td> 78 - </tr> 79 - ))} 90 + {isLoading 91 + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( 92 + <tr key={`skeleton-${i}`}> 93 + <td colSpan={3} className="p-0"> 94 + <SecretRowSkeleton index={i} /> 95 + </td> 96 + </tr> 97 + )) 98 + : secrets?.secrets?.map((secret) => ( 99 + <tr key={secret.id}> 100 + <td className="normal-case text-[14px] font-medium"> 101 + {secret.name} 102 + </td> 103 + <td className="normal-case text-[14px] font-medium"> 104 + {dayjs(secret.createdAt).format( 105 + "M/D/YYYY, h:mm:ss A", 106 + )} 107 + </td> 108 + <td className="normal-case text-[14px]"></td> 109 + </tr> 110 + ))} 80 111 </tbody> 81 112 </table> 82 113 </div>
+48 -15
apps/web/src/pages/settings/variables/Variables.tsx
··· 1 + import ContentLoader from "react-content-loader"; 1 2 import { useRouterState } from "@tanstack/react-router"; 2 3 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 4 import Main from "../../../layouts/Main"; ··· 9 10 import Pagination from "../../../components/pagination"; 10 11 11 12 const PAGE_SIZE = 10; 13 + const SKELETON_ROWS = 8; 14 + 15 + const VariableRowSkeleton = ({ index }: { index: number }) => ( 16 + <ContentLoader 17 + speed={1.5} 18 + width="100%" 19 + height={48} 20 + backgroundColor="oklch(var(--b2))" 21 + foregroundColor="oklch(var(--b3))" 22 + style={{ width: "100%" }} 23 + uniqueKey={`variable-row-skeleton-${index}`} 24 + > 25 + {/* Name column */} 26 + <rect x="16" y="16" rx="6" ry="6" width="20%" height="16" /> 27 + {/* Value column */} 28 + <rect x="28%" y="16" rx="6" ry="6" width="24%" height="16" /> 29 + {/* Created At column */} 30 + <rect x="58%" y="16" rx="6" ry="6" width="22%" height="16" /> 31 + {/* Action column */} 32 + <rect x="88%" y="12" rx="6" ry="6" width="8%" height="24" /> 33 + </ContentLoader> 34 + ); 12 35 13 36 function Variables() { 14 37 const [isOpen, setIsOpen] = useState(false); ··· 20 43 const { data } = useSandboxQuery( 21 44 `at:/${pathname.replace("/variables", "").replace("sandbox", "io.pocketenv.sandbox")}`, 22 45 ); 23 - const { data: variables } = useVariablesQuery( 46 + const { data: variables, isLoading } = useVariablesQuery( 24 47 data?.sandbox?.id, 25 48 offset, 26 49 PAGE_SIZE, ··· 68 91 </tr> 69 92 </thead> 70 93 <tbody> 71 - {variables?.variables?.map((variable) => ( 72 - <tr key={variable.id}> 73 - <td className="normal-case text-[14px] font-medium"> 74 - {variable.name} 75 - </td> 76 - <td className="normal-case text-[14px] font-medium"> 77 - {variable.value} 78 - </td> 79 - <td className="normal-case text-[14px] font-medium"> 80 - {dayjs(variable.createdAt).format("M/D/YYYY, h:mm:ss A")} 81 - </td> 82 - <td className="normal-case text-[14px] font-medium"></td> 83 - </tr> 84 - ))} 94 + {isLoading 95 + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( 96 + <tr key={`skeleton-${i}`}> 97 + <td colSpan={4} className="p-0"> 98 + <VariableRowSkeleton index={i} /> 99 + </td> 100 + </tr> 101 + )) 102 + : variables?.variables?.map((variable) => ( 103 + <tr key={variable.id}> 104 + <td className="normal-case text-[14px] font-medium"> 105 + {variable.name} 106 + </td> 107 + <td className="normal-case text-[14px] font-medium"> 108 + {variable.value} 109 + </td> 110 + <td className="normal-case text-[14px] font-medium"> 111 + {dayjs(variable.createdAt).format( 112 + "M/D/YYYY, h:mm:ss A", 113 + )} 114 + </td> 115 + <td className="normal-case text-[14px] font-medium"></td> 116 + </tr> 117 + ))} 85 118 </tbody> 86 119 </table> 87 120 </div>
+44 -11
apps/web/src/pages/settings/volumes/Volumes.tsx
··· 1 + import ContentLoader from "react-content-loader"; 1 2 import { useRouterState } from "@tanstack/react-router"; 2 3 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 4 import Main from "../../../layouts/Main"; ··· 9 10 import Pagination from "../../../components/pagination"; 10 11 11 12 const PAGE_SIZE = 12; 13 + const SKELETON_ROWS = 8; 14 + 15 + const VolumeRowSkeleton = ({ index }: { index: number }) => ( 16 + <ContentLoader 17 + speed={1.5} 18 + width="100%" 19 + height={48} 20 + backgroundColor="oklch(var(--b2))" 21 + foregroundColor="oklch(var(--b3))" 22 + style={{ width: "100%" }} 23 + uniqueKey={`volume-row-skeleton-${index}`} 24 + > 25 + {/* Name column */} 26 + <rect x="16" y="16" rx="6" ry="6" width="20%" height="16" /> 27 + {/* Mount Path column */} 28 + <rect x="28%" y="16" rx="6" ry="6" width="24%" height="16" /> 29 + {/* Created At column */} 30 + <rect x="58%" y="16" rx="6" ry="6" width="22%" height="16" /> 31 + {/* Action column */} 32 + <rect x="88%" y="12" rx="6" ry="6" width="8%" height="24" /> 33 + </ContentLoader> 34 + ); 12 35 13 36 function Volumes() { 14 37 const [isOpen, setIsOpen] = useState(false); ··· 20 43 const { data } = useSandboxQuery( 21 44 `at:/${pathname.replace("/volumes", "").replace("sandbox", "io.pocketenv.sandbox")}`, 22 45 ); 23 - const { data: volumes } = useVolumesQuery( 46 + const { data: volumes, isLoading } = useVolumesQuery( 24 47 data?.sandbox?.id, 25 48 offset, 26 49 PAGE_SIZE, ··· 67 90 </tr> 68 91 </thead> 69 92 <tbody> 70 - {volumes?.volumes?.map((volume) => ( 71 - <tr key={volume.id}> 72 - <td>{volume.name}</td> 73 - <td>{volume.path}</td> 74 - <td> 75 - {dayjs(volume.createdAt).format("M/D/YYYY, h:mm:ss A")} 76 - </td> 77 - <td></td> 78 - </tr> 79 - ))} 93 + {isLoading 94 + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( 95 + <tr key={`skeleton-${i}`}> 96 + <td colSpan={4} className="p-0"> 97 + <VolumeRowSkeleton index={i} /> 98 + </td> 99 + </tr> 100 + )) 101 + : volumes?.volumes?.map((volume) => ( 102 + <tr key={volume.id}> 103 + <td>{volume.name}</td> 104 + <td>{volume.path}</td> 105 + <td> 106 + {dayjs(volume.createdAt).format( 107 + "M/D/YYYY, h:mm:ss A", 108 + )} 109 + </td> 110 + <td></td> 111 + </tr> 112 + ))} 80 113 </tbody> 81 114 </table> 82 115 </div>