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 actor sandboxes endpoint and UI

Introduce io.pocketenv.actor.getActorSandboxes lexicon and XRPC
handler with DB retrieval and presentation. Add client API call,
React hook, Projects page and Project component to display an actor's
sandboxes. Remove the deprecated "author" param from getSandboxes and
add a sidebar-collapsed atom for UI state.

+501 -59
+53
apps/api/lexicons/actor/getActorSandboxes.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.actor.getActorSandboxes", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get all sandboxes for a given actor", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "did" 12 + ], 13 + "properties": { 14 + "did": { 15 + "type": "string", 16 + "description": "The DID or handle of the actor", 17 + "format": "at-identifier" 18 + }, 19 + "limit": { 20 + "type": "integer", 21 + "description": "The maximum number of sandboxes to return.", 22 + "minimum": 1 23 + }, 24 + "offset": { 25 + "type": "integer", 26 + "description": "The number of sandboxes to skip before starting to collect the result set.", 27 + "minimum": 0 28 + } 29 + } 30 + }, 31 + "output": { 32 + "encoding": "application/json", 33 + "schema": { 34 + "type": "object", 35 + "properties": { 36 + "sandboxes": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "io.pocketenv.sandbox.defs#sandboxViewDetailed" 41 + } 42 + }, 43 + "total": { 44 + "type": "integer", 45 + "description": "The total number of sandboxes available.", 46 + "minimum": 0 47 + } 48 + } 49 + } 50 + } 51 + } 52 + } 53 + }
-5
apps/api/lexicons/sandbox/getSandboxes.json
··· 8 8 "parameters": { 9 9 "type": "params", 10 10 "properties": { 11 - "author": { 12 - "type": "string", 13 - "description": "Filter sandboxes by author did or handle", 14 - "format": "at-identifier" 15 - }, 16 11 "limit": { 17 12 "type": "integer", 18 13 "description": "The maximum number of sandboxes to return.",
+49
apps/api/pkl/defs/actor/getActorSandboxes.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.actor.getActorSandboxes" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "Get all sandboxes for a given actor" 9 + parameters { 10 + type = "params" 11 + required = List("did") 12 + properties { 13 + ["did"] = new StringType { 14 + description = "The DID or handle of the actor" 15 + format = "at-identifier" 16 + } 17 + ["limit"] = new IntegerType { 18 + type = "integer" 19 + description = "The maximum number of sandboxes to return." 20 + minimum = 1 21 + } 22 + ["offset"] = new IntegerType { 23 + type = "integer" 24 + description = "The number of sandboxes to skip before starting to collect the result set." 25 + minimum = 0 26 + } 27 + } 28 + } 29 + output { 30 + encoding = "application/json" 31 + schema = new ObjectType { 32 + type = "object" 33 + properties { 34 + ["sandboxes"] = new Array { 35 + type = "array" 36 + items = new Ref { 37 + ref = "io.pocketenv.sandbox.defs#sandboxViewDetailed" 38 + } 39 + } 40 + ["total"] = new IntegerType { 41 + type = "integer" 42 + description = "The total number of sandboxes available." 43 + minimum = 0 44 + } 45 + } 46 + } 47 + } 48 + } 49 + }
-5
apps/api/pkl/defs/sandbox/getSandboxes.pkl
··· 9 9 parameters { 10 10 type = "params" 11 11 properties { 12 - ["author"] = new StringType { 13 - type = "string" 14 - description = "Filter sandboxes by author did or handle" 15 - format = "at-identifier" 16 - } 17 12 ["limit"] = new IntegerType { 18 13 type = "integer" 19 14 description = "The maximum number of sandboxes to return."
+12
apps/api/src/lexicon/index.ts
··· 9 9 type StreamAuthVerifier, 10 10 } from "@atproto/xrpc-server"; 11 11 import { schemas } from "./lexicons"; 12 + import type * as IoPocketenvActorGetActorSandboxes from "./types/io/pocketenv/actor/getActorSandboxes"; 12 13 import type * as IoPocketenvActorGetProfile from "./types/io/pocketenv/actor/getProfile"; 13 14 import type * as IoPocketenvSandboxClaimSandbox from "./types/io/pocketenv/sandbox/claimSandbox"; 14 15 import type * as IoPocketenvSandboxCreateSandbox from "./types/io/pocketenv/sandbox/createSandbox"; ··· 63 64 64 65 constructor(server: Server) { 65 66 this._server = server; 67 + } 68 + 69 + getActorSandboxes<AV extends AuthVerifier>( 70 + cfg: ConfigOf< 71 + AV, 72 + IoPocketenvActorGetActorSandboxes.Handler<ExtractAuth<AV>>, 73 + IoPocketenvActorGetActorSandboxes.HandlerReqCtx<ExtractAuth<AV>> 74 + >, 75 + ) { 76 + const nsid = "io.pocketenv.actor.getActorSandboxes"; // @ts-ignore 77 + return this._server.xrpc.method(nsid, cfg); 66 78 } 67 79 68 80 getProfile<AV extends AuthVerifier>(
+53 -5
apps/api/src/lexicon/lexicons.ts
··· 46 46 }, 47 47 }, 48 48 }, 49 + IoPocketenvActorGetActorSandboxes: { 50 + lexicon: 1, 51 + id: "io.pocketenv.actor.getActorSandboxes", 52 + defs: { 53 + main: { 54 + type: "query", 55 + description: "Get all sandboxes for a given actor", 56 + parameters: { 57 + type: "params", 58 + required: ["did"], 59 + properties: { 60 + did: { 61 + type: "string", 62 + description: "The DID or handle of the actor", 63 + format: "at-identifier", 64 + }, 65 + limit: { 66 + type: "integer", 67 + description: "The maximum number of sandboxes to return.", 68 + minimum: 1, 69 + }, 70 + offset: { 71 + type: "integer", 72 + description: 73 + "The number of sandboxes to skip before starting to collect the result set.", 74 + minimum: 0, 75 + }, 76 + }, 77 + }, 78 + output: { 79 + encoding: "application/json", 80 + schema: { 81 + type: "object", 82 + properties: { 83 + sandboxes: { 84 + type: "array", 85 + items: { 86 + type: "ref", 87 + ref: "lex:io.pocketenv.sandbox.defs#sandboxViewDetailed", 88 + }, 89 + }, 90 + total: { 91 + type: "integer", 92 + description: "The total number of sandboxes available.", 93 + minimum: 0, 94 + }, 95 + }, 96 + }, 97 + }, 98 + }, 99 + }, 100 + }, 49 101 IoPocketenvActorGetProfile: { 50 102 lexicon: 1, 51 103 id: "io.pocketenv.actor.getProfile", ··· 553 605 parameters: { 554 606 type: "params", 555 607 properties: { 556 - author: { 557 - type: "string", 558 - description: "Filter sandboxes by author did or handle", 559 - format: "at-identifier", 560 - }, 561 608 limit: { 562 609 type: "integer", 563 610 description: "The maximum number of sandboxes to return.", ··· 790 837 export const lexicons: Lexicons = new Lexicons(schemas); 791 838 export const ids = { 792 839 IoPocketenvActorDefs: "io.pocketenv.actor.defs", 840 + IoPocketenvActorGetActorSandboxes: "io.pocketenv.actor.getActorSandboxes", 793 841 IoPocketenvActorGetProfile: "io.pocketenv.actor.getProfile", 794 842 AppBskyActorProfile: "app.bsky.actor.profile", 795 843 IoPocketenvSandboxClaimSandbox: "io.pocketenv.sandbox.claimSandbox",
+54
apps/api/src/lexicon/types/io/pocketenv/actor/getActorSandboxes.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as IoPocketenvSandboxDefs from "../sandbox/defs"; 11 + 12 + export interface QueryParams { 13 + /** The DID or handle of the actor */ 14 + did: string; 15 + /** The maximum number of sandboxes to return. */ 16 + limit?: number; 17 + /** The number of sandboxes to skip before starting to collect the result set. */ 18 + offset?: number; 19 + } 20 + 21 + export type InputSchema = undefined; 22 + 23 + export interface OutputSchema { 24 + sandboxes?: IoPocketenvSandboxDefs.SandboxViewDetailed[]; 25 + /** The total number of sandboxes available. */ 26 + total?: number; 27 + [k: string]: unknown; 28 + } 29 + 30 + export type HandlerInput = undefined; 31 + 32 + export interface HandlerSuccess { 33 + encoding: "application/json"; 34 + body: OutputSchema; 35 + headers?: { [key: string]: string }; 36 + } 37 + 38 + export interface HandlerError { 39 + status: number; 40 + message?: string; 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA; 46 + params: QueryParams; 47 + input: HandlerInput; 48 + req: express.Request; 49 + res: express.Response; 50 + resetRouteRateLimits: () => Promise<void>; 51 + }; 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput;
-2
apps/api/src/lexicon/types/io/pocketenv/sandbox/getSandboxes.ts
··· 10 10 import type * as IoPocketenvSandboxDefs from "./defs"; 11 11 12 12 export interface QueryParams { 13 - /** Filter sandboxes by author did or handle */ 14 - author?: string; 15 13 /** The maximum number of sandboxes to return. */ 16 14 limit?: number; 17 15 /** The number of sandboxes to skip before starting to collect the result set. */
+2
apps/api/src/xrpc/index.ts
··· 8 8 import stopSandbox from "./io/pocketenv/sandbox/stopSandbox"; 9 9 import claimSandbox from "./io/pocketenv/sandbox/claimSandbox"; 10 10 import getProfile from "./io/pocketenv/actor/getProfile"; 11 + import getActorSandboxes from "./io/pocketenv/actor/getActorSandboxes"; 11 12 12 13 export default function (server: Server, ctx: Context) { 13 14 // io.pocketenv 14 15 getSandbox(server, ctx); 15 16 getSandboxes(server, ctx); 17 + getActorSandboxes(server, ctx); 16 18 createSandbox(server, ctx); 17 19 deleteSandbox(server, ctx); 18 20 startSandbox(server, ctx);
+106
apps/api/src/xrpc/io/pocketenv/actor/getActorSandboxes.ts
··· 1 + import type { HandlerAuth } from "@atproto/xrpc-server"; 2 + import type { Context } from "context"; 3 + import type { Server } from "lexicon"; 4 + import { Effect, pipe } from "effect"; 5 + import type { 6 + QueryParams, 7 + OutputSchema, 8 + } from "lexicon/types/io/pocketenv/actor/getActorSandboxes"; 9 + import type { SelectSandbox } from "schema/sandboxes"; 10 + import { consola } from "consola"; 11 + import schema from "schema"; 12 + import { count, eq, desc, or } from "drizzle-orm"; 13 + 14 + export default function (server: Server, ctx: Context) { 15 + const getActorSandboxes = (params: QueryParams, auth: HandlerAuth) => 16 + pipe( 17 + { params, ctx }, 18 + retrieve, 19 + Effect.flatMap(presentation), 20 + Effect.retry({ times: 3 }), 21 + Effect.timeout("10 seconds"), 22 + Effect.catchAll((err) => { 23 + consola.error("Error retrieving sandboxes:", err); 24 + return Effect.succeed({ sandboxes: [] }); 25 + }), 26 + ); 27 + server.io.pocketenv.actor.getActorSandboxes({ 28 + auth: ctx.authVerifier, 29 + handler: async ({ params, auth }) => { 30 + const result = await Effect.runPromise(getActorSandboxes(params, auth)); 31 + return { 32 + encoding: "application/json", 33 + body: result, 34 + }; 35 + }, 36 + }); 37 + } 38 + 39 + const retrieve = ({ 40 + params, 41 + ctx, 42 + }: { 43 + params: QueryParams; 44 + ctx: Context; 45 + }): Effect.Effect<[SelectSandbox[], number], Error> => { 46 + return Effect.tryPromise({ 47 + try: async () => 48 + Promise.all([ 49 + ctx.db 50 + .select() 51 + .from(schema.sandboxes) 52 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 53 + .where( 54 + or( 55 + eq(schema.users.did, params.did), 56 + eq(schema.users.handle, params.did), 57 + ), 58 + ) 59 + .orderBy(desc(schema.sandboxes.installs)) 60 + .limit(params.limit ?? 30) 61 + .offset(params.offset ?? 0) 62 + .execute() 63 + .then((result) => result.map((row) => row.sandboxes)), 64 + ctx.db 65 + .select({ count: count() }) 66 + .from(schema.sandboxes) 67 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 68 + .where( 69 + or( 70 + eq(schema.users.did, params.did), 71 + eq(schema.users.handle, params.did), 72 + ), 73 + ) 74 + .execute() 75 + .then((result) => result[0]?.count ?? 0), 76 + ]), 77 + catch: (error) => 78 + new Error( 79 + `Failed to retrieve sandboxes: ${error instanceof Error ? error.message : String(error)}`, 80 + ), 81 + }); 82 + }; 83 + 84 + const presentation = ([sandboxes, total]: [ 85 + SelectSandbox[], 86 + number, 87 + ]): Effect.Effect<OutputSchema, never> => { 88 + return Effect.sync(() => ({ 89 + sandboxes: sandboxes.map((sandbox) => ({ 90 + id: sandbox.id, 91 + name: sandbox.name, 92 + displayName: sandbox.displayName, 93 + description: sandbox.description!, 94 + logo: sandbox.logo!, 95 + readme: sandbox.readme!, 96 + status: sandbox.status, 97 + installs: sandbox.installs, 98 + uri: sandbox.uri, 99 + vcpus: sandbox.vcpus as number, 100 + memory: sandbox.memory as number, 101 + disk: sandbox.disk as number, 102 + createdAt: sandbox.createdAt.toISOString(), 103 + })), 104 + total, 105 + })); 106 + };
+2 -16
apps/api/src/xrpc/io/pocketenv/sandbox/getSandboxes.ts
··· 50 50 .select() 51 51 .from(schema.sandboxes) 52 52 .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 53 - .where( 54 - params.author 55 - ? or( 56 - eq(schema.users.did, params.author), 57 - eq(schema.users.handle, params.author), 58 - ) 59 - : eq(schema.users.handle, "pocketenv.io"), 60 - ) 53 + .where(eq(schema.users.handle, "pocketenv.io")) 61 54 .orderBy(desc(schema.sandboxes.installs)) 62 55 .limit(params.limit ?? 30) 63 56 .offset(params.offset ?? 0) ··· 67 60 .select({ count: count() }) 68 61 .from(schema.sandboxes) 69 62 .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 70 - .where( 71 - params.author 72 - ? or( 73 - eq(schema.users.did, params.author), 74 - eq(schema.users.handle, params.author), 75 - ) 76 - : eq(schema.users.handle, "pocketenv.io"), 77 - ) 63 + .where(eq(schema.users.handle, "pocketenv.io")) 78 64 .execute() 79 65 .then((result) => result[0]?.count ?? 0), 80 66 ]),
+9
apps/web/src/api/sandbox.ts
··· 37 37 `/xrpc/io.pocketenv.sandbox.getSandboxes?offset=${offset ?? 0}&limit=${limit ?? 30}`, 38 38 ); 39 39 40 + export const getActorSandboxes = ( 41 + did: string, 42 + offset?: number, 43 + limit?: number, 44 + ) => 45 + client.get<{ sandboxes: Sandbox[]; total: number }>( 46 + `/xrpc/io.pocketenv.actor.getActorSandboxes?did=${did}&offset=${offset ?? 0}&limit=${limit ?? 30}`, 47 + ); 48 + 40 49 export const stopSandbox = (id: string) => 41 50 client.post(`/xrpc/io.pocketenv.sandbox.stopSandbox?id=${id}`, undefined, { 42 51 headers: {
+3
apps/web/src/atoms/sidebar.ts
··· 1 + import { atomWithStorage } from "jotai/utils"; 2 + 3 + export const sidebarCollapsedAtom = atomWithStorage("sidebar-collapsed", false);
+44 -16
apps/web/src/components/sidebar/Sidebar.tsx
··· 1 1 import { Link, useRouterState } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import { sidebarCollapsedAtom } from "../../atoms/sidebar"; 2 4 import Logo from "../../assets/logo.png"; 3 5 4 6 function Sidebar() { 5 7 const routerState = useRouterState(); 6 8 const pathname = routerState.location.pathname; 9 + const [isCollapsed, setIsCollapsed] = useAtom(sidebarCollapsedAtom); 10 + 11 + const toggleSidebar = () => { 12 + setIsCollapsed((prev) => !prev); 13 + }; 7 14 8 15 // Check if a route is active 9 16 const isActive = (path: string): boolean => { ··· 12 19 } 13 20 return pathname === path; 14 21 }; 22 + 15 23 return ( 16 24 <div> 17 25 <aside 18 - id="scoped-sidebar" 19 - className="overlay [--auto-close:sm] sm:shadow-none overlay-open:translate-x-0 drawer drawer-start max-w-64 fixed z-50 sm:flex sm:translate-x-0 [--body-scroll:true] h-screen bg-base-100" 26 + className={`fixed z-50 h-screen bg-base-100 w-64 transition-transform duration-300 ease-in-out ${ 27 + isCollapsed ? "-translate-x-48" : "translate-x-0" 28 + }`} 20 29 role="dialog" 21 - tabIndex="-1" 30 + tabIndex={-1} 22 31 > 23 - <div className="drawer-body px-2 pt-4 bg-base-100"> 24 - <Link to="/projects"> 25 - <div className="mb-[30px] ml-[5px]"> 26 - <img src={Logo} className="max-h-[40px] mr-[15px]" /> 32 + <div className="drawer-body px-2 pt-4 bg-base-100 h-full"> 33 + <div className="flex items-center"> 34 + <div className="flex-1"> 35 + <Link to="/projects"> 36 + <div className="mb-[30px] ml-[5px]"> 37 + <img src={Logo} className="max-h-[40px] mr-[15px]" /> 38 + </div> 39 + </Link> 27 40 </div> 28 - </Link> 41 + <button 42 + className="mb-[25px] opacity-70 hover:opacity-100 transition-opacity" 43 + onClick={toggleSidebar} 44 + aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} 45 + > 46 + <span 47 + className={`icon-[hugeicons--panel-left] size-5.5 text-white transition-transform duration-300 ${ 48 + isCollapsed ? "rotate-180" : "" 49 + }`} 50 + ></span> 51 + </button> 52 + </div> 29 53 <ul className="menu p-0"> 30 54 <li> 31 55 <Link 32 56 to="/projects" 33 57 className={ 34 58 isActive("/projects") 35 - ? "active bg-white/7 text-[#ff41b5]! font-semibold rounded-full" 36 - : "rounded-full" 59 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 60 + : "rounded-full hover:text-white" 37 61 } 62 + title="Projects" 38 63 > 39 64 <span className="icon-[tabler--box] size-6 mr-2"></span> 40 65 Projects ··· 45 70 to="/snapshots" 46 71 className={ 47 72 isActive("/snapshots") 48 - ? "active bg-white/7 text-[#ff41b5]! font-semibold rounded-full" 49 - : "rounded-full" 73 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 74 + : "rounded-full hover:text-white" 50 75 } 76 + title="Snapshots" 51 77 > 52 78 <span className="icon-[tabler--device-floppy] size-6 mr-2"></span> 53 79 Snapshots ··· 58 84 to="/volumes" 59 85 className={ 60 86 isActive("/volumes") 61 - ? "active bg-white/7 text-[#ff41b5]! font-semibold rounded-full" 62 - : "rounded-full" 87 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 88 + : "rounded-full hover:text-white" 63 89 } 90 + title="Volumes" 64 91 > 65 92 <span className="icon-[icon-park-outline--hard-disk] size-5 mr-2"></span> 66 93 Volumes ··· 71 98 to="/secrets" 72 99 className={ 73 100 isActive("/secrets") 74 - ? "active bg-white/7 text-[#ff41b5]! font-semibold rounded-full" 75 - : "rounded-full" 101 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 102 + : "rounded-full hover:text-white" 76 103 } 104 + title="Secrets" 77 105 > 78 106 <span className="icon-[tabler--key] size-6 mr-2"></span> 79 107 Secrets
+13
apps/web/src/hooks/useSandbox.ts
··· 3 3 claimSandbox, 4 4 createSandbox, 5 5 deleteSandbox, 6 + getActorSandboxes, 6 7 getSandbox, 7 8 getSandboxes, 8 9 startSandbox, 9 10 stopSandbox, 10 11 } from "../api/sandbox"; 12 + 13 + export const useActorSandboxesQuery = ( 14 + did: string, 15 + offset?: number, 16 + limit?: number, 17 + ) => 18 + useQuery({ 19 + queryKey: ["actorSandboxes", did, offset, limit], 20 + queryFn: () => getActorSandboxes(did, offset, limit), 21 + select: (response) => response.data, 22 + enabled: !!did, 23 + }); 11 24 12 25 export const useSandboxesQuery = (offset?: number, limit?: number) => 13 26 useQuery({
+13 -8
apps/web/src/layouts/Main.tsx
··· 1 1 import type React from "react"; 2 2 import { useNavigate, useRouterState } from "@tanstack/react-router"; 3 + import { useAtomValue } from "jotai"; 3 4 import Navbar from "../components/navbar"; 4 5 import Sidebar from "../components/sidebar"; 6 + import { sidebarCollapsedAtom } from "../atoms/sidebar"; 5 7 6 8 type MainProps = { 7 9 children: React.ReactNode; ··· 12 14 const navigate = useNavigate(); 13 15 const pathname = routerState.location.pathname; 14 16 const isAuthenticated = !!localStorage.getItem("token"); 17 + const isCollapsed = useAtomValue(sidebarCollapsedAtom); 15 18 16 19 if (!isAuthenticated) { 17 20 navigate({ to: "/" }); ··· 31 34 const title = getTitle(pathname); 32 35 33 36 return ( 34 - <> 35 - <div className="flex min-h-screen bg-base-100"> 36 - <Sidebar /> 37 - <div className="flex flex-col flex-1 sm:ml-64 bg-base-100"> 38 - <Navbar title={title} /> 39 - <main className="flex-1 p-4 bg-base-100">{children}</main> 40 - </div> 37 + <div className="flex min-h-screen bg-base-100"> 38 + <Sidebar /> 39 + <div 40 + className={`flex flex-col flex-1 bg-base-100 transition-all duration-300 ease-in-out ${ 41 + isCollapsed ? "sm:ml-16" : "sm:ml-64" 42 + }`} 43 + > 44 + <Navbar title={title} /> 45 + <main className="flex-1 p-4 bg-base-100">{children}</main> 41 46 </div> 42 - </> 47 + </div> 43 48 ); 44 49 } 45 50
+57
apps/web/src/pages/projects/Project/Project.tsx
··· 1 + import type { Sandbox } from "../../../types/sandbox"; 2 + import _ from "lodash"; 3 + 4 + export type ProjectProps = { 5 + sandbox: Sandbox; 6 + }; 7 + 8 + function Project({ sandbox }: ProjectProps) { 9 + const onPlay = (e: React.MouseEvent) => { 10 + e.stopPropagation(); 11 + }; 12 + 13 + const onStop = (e: React.MouseEvent) => { 14 + e.stopPropagation(); 15 + }; 16 + 17 + const onOpenProject = () => {}; 18 + 19 + return ( 20 + <tr className="cursor-pointer" onClick={onOpenProject}> 21 + <td>{sandbox.name}</td> 22 + <td> 23 + <span 24 + className={`badge badge-soft ${sandbox?.status === "RUNNING" ? "badge-success" : ""} rounded-full ${sandbox.status === "RUNNING" ? "bg-green-400/10" : "bg-white/15 rounded"}`} 25 + > 26 + {_.upperFirst(_.camelCase(sandbox.status))} 27 + </span> 28 + </td> 29 + <td> 30 + <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full"> 31 + {sandbox?.vcpus} CPU 32 + </span> 33 + <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full ml-2"> 34 + {sandbox?.memory} GiB RAM 35 + </span> 36 + </td> 37 + <td>March 1, 2024</td> 38 + <td> 39 + {sandbox.status === "RUNNING" && ( 40 + <button className="btn btn-circle btn-text btn-sm" onClick={onStop}> 41 + <span className="icon-[tabler--player-stop] size-5"></span> 42 + </button> 43 + )} 44 + {sandbox.status !== "RUNNING" && ( 45 + <button className="btn btn-circle btn-text btn-sm" onClick={onPlay}> 46 + <span className="icon-[tabler--player-play] size-5"></span> 47 + </button> 48 + )} 49 + <button className="btn btn-circle btn-text btn-sm"> 50 + <span className="icon-[tabler--dots-vertical] size-5"></span> 51 + </button> 52 + </td> 53 + </tr> 54 + ); 55 + } 56 + 57 + export default Project;
+3
apps/web/src/pages/projects/Project/index.tsx
··· 1 + import Project from "./Project"; 2 + 3 + export default Project;
+28 -2
apps/web/src/pages/projects/Projects.tsx
··· 1 + import { useAtomValue } from "jotai"; 2 + import { profileAtom } from "../../atoms/profile"; 3 + import { useActorSandboxesQuery } from "../../hooks/useSandbox"; 1 4 import Main from "../../layouts/Main"; 5 + import _ from "lodash"; 6 + import Project from "./Project"; 2 7 3 8 function Projects() { 9 + const profile = useAtomValue(profileAtom); 10 + const { data, isLoading } = useActorSandboxesQuery(profile?.did || ""); 4 11 return ( 5 12 <Main> 6 - {/* Your page content goes here */} 7 - <></> 13 + <div> 14 + <div className="w-full overflow-x-auto"> 15 + <table className="table"> 16 + <thead> 17 + <tr> 18 + <th className="normal-case text-[14px]">Name</th> 19 + <th className="normal-case text-[14px]">State</th> 20 + <th className="normal-case text-[14px]">Resources</th> 21 + <th className="normal-case text-[14px]">Created At</th> 22 + <th className="normal-case text-[14px]"></th>{" "} 23 + </tr> 24 + </thead> 25 + <tbody> 26 + {!isLoading && 27 + data?.sandboxes?.map((sandbox) => ( 28 + <Project sandbox={sandbox} key={sandbox.id} /> 29 + ))} 30 + </tbody> 31 + </table> 32 + </div> 33 + </div> 8 34 </Main> 9 35 ); 10 36 }