Skip to content

Commit 41981e3

Browse files
committed
feat: start image pull admin dash
1 parent dfc541b commit 41981e3

File tree

10 files changed

+226
-88
lines changed

10 files changed

+226
-88
lines changed

apps/daemon/src/workspace/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { pullImage } from "./pull";
44
const statuses = t.Union([
55
t.Literal("pulled"),
66
t.Literal("in-progress"),
7-
t.Literal("not-started"),
87
t.Literal("failed"),
98
t.Literal("not-touched"),
109
]);

apps/web/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@
3333
"@radix-ui/react-tooltip": "^1.1.6",
3434
"@scalar/nextjs-api-reference": "^0.5.4",
3535
"@stardust/common": "workspace:*",
36-
"@stardust/theme": "workspace:*",
3736
"@stardust/config": "workspace:*",
3837
"@stardust/db": "workspace:*",
38+
"@stardust/theme": "workspace:*",
3939
"@stardust/tsconfig": "workspace:*",
4040
"@tanstack/react-table": "^8.20.6",
4141
"autoprefixer": "^10.4.20",
4242
"class-variance-authority": "^0.7.0",
4343
"clsx": "^2.1.1",
4444
"geist": "^1.3.1",
4545
"lucide-react": "^0.460.0",
46-
"next": "15.1.5",
46+
"next": "15.1.6",
4747
"next-themes": "^0.4.4",
4848
"postcss": "^8.4.49",
4949
"react": "19.0.0-rc-66855b96-20241106",

apps/web/src/app/(main)/admin/workspaces/actions.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"use server";
22

33
import { check } from "@/lib/admin-check";
4-
import db, { workspace } from "@stardust/db";
4+
import { stardustConnector } from "@stardust/common/daemon/client";
5+
import { getConfig } from "@stardust/config";
6+
import db, { type SelectWorkspace, workspace } from "@stardust/db";
57
import { redirect } from "next/navigation";
68

79
export async function updateWorkspace(data: FormData) {
@@ -30,3 +32,29 @@ export async function updateWorkspace(data: FormData) {
3032
});
3133
redirect("/admin/workspaces");
3234
}
35+
export async function pullOnNode(workspace: SelectWorkspace, nId: string) {
36+
await check();
37+
const node = getConfig().nodes.find((n) => n.id === nId);
38+
if (!node) {
39+
throw new Error("Node not found");
40+
}
41+
const connector = stardustConnector(node);
42+
// todo: i need to fix the fact it needs to be in the query param
43+
const { data, error } = await connector.workspaces.create.put(
44+
{ image: workspace.dockerImage },
45+
{ query: { id: workspace.dockerImage } },
46+
);
47+
if (error) throw new Error(error.value.message);
48+
return data;
49+
}
50+
export async function deleteImageFromNode(workspace: SelectWorkspace, nId: string) {
51+
await check();
52+
const node = getConfig().nodes.find((n) => n.id === nId);
53+
if (!node) {
54+
throw new Error("Node not found");
55+
}
56+
const connector = stardustConnector(node);
57+
const { data, error } = await connector.workspaces.info.delete(undefined, { query: { id: workspace.dockerImage } });
58+
if (error) throw new Error(error.value.message);
59+
return data;
60+
}

apps/web/src/app/(main)/admin/workspaces/columns.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import {
77
DropdownMenuLabel,
88
DropdownMenuTrigger,
99
} from "@/components/ui/dropdown-menu";
10+
import { getNodeWorkspaces } from "@/lib/workspaces";
1011
import type { SelectWorkspaceRelation } from "@stardust/db/relational-types";
1112
import type { ColumnDef } from "@tanstack/react-table";
1213
import { MoreHorizontal } from "lucide-react";
1314
import Image from "next/image";
1415
import { useState } from "react";
15-
import { UpdateDialog } from "./components";
16+
import { NodeDialog, UpdateDialog } from "./components";
1617

1718
export const columns: ColumnDef<SelectWorkspaceRelation & { nodes: string[] }>[] = [
1819
{
@@ -41,10 +42,12 @@ export const columns: ColumnDef<SelectWorkspaceRelation & { nodes: string[] }>[]
4142
{
4243
id: "actions",
4344
cell: ({ row: { original: workspace } }) => {
44-
const [open, setOpen] = useState(false);
45+
const [updateDialogOpen, setUpdateDialogOpen] = useState(false);
46+
const [nodeDialogOpen, setNodeDialogOpen] = useState(false);
4547
return (
4648
<>
47-
<UpdateDialog workspace={workspace} open={open} setOpen={setOpen} />
49+
<UpdateDialog workspace={workspace} open={updateDialogOpen} setOpen={setUpdateDialogOpen} />
50+
<NodeDialog workspace={workspace} open={nodeDialogOpen} setOpen={setNodeDialogOpen} />
4851
<DropdownMenu>
4952
<DropdownMenuTrigger asChild>
5053
<Button variant="ghost" className="h-8 w-8 p-0">
@@ -54,7 +57,8 @@ export const columns: ColumnDef<SelectWorkspaceRelation & { nodes: string[] }>[]
5457
</DropdownMenuTrigger>
5558
<DropdownMenuContent align="end">
5659
<DropdownMenuLabel>Actions</DropdownMenuLabel>
57-
<DropdownMenuItem onClick={() => setOpen(true)}>Edit metadata</DropdownMenuItem>
60+
<DropdownMenuItem onClick={() => setUpdateDialogOpen(true)}>Edit metadata</DropdownMenuItem>
61+
<DropdownMenuItem onClick={() => setNodeDialogOpen(true)}>Edit nodes</DropdownMenuItem>
5862
</DropdownMenuContent>
5963
</DropdownMenu>
6064
</>

apps/web/src/app/(main)/admin/workspaces/components.tsx

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1+
"use client";
12
import { SubmitButton } from "@/components/submit-button";
3+
import { Button } from "@/components/ui/button";
4+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
25
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
36
import { Input } from "@/components/ui/input";
47
import { Label } from "@/components/ui/label";
8+
import { fetcher } from "@/lib/utils";
9+
import type { getNodeWorkspaces } from "@/lib/workspaces";
510
import type { SelectWorkspace } from "@stardust/db";
6-
import { updateWorkspace } from "./actions";
7-
export function UpdateDialog({
8-
workspace,
9-
open,
10-
setOpen,
11-
}: {
11+
import { toast } from "sonner";
12+
import useSWR from "swr";
13+
import { deleteImageFromNode, pullOnNode, updateWorkspace } from "./actions";
14+
export interface Props {
1215
workspace: SelectWorkspace;
1316
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
1417
open: boolean;
15-
}) {
18+
}
19+
const statesMap: Record<
20+
// world class code
21+
Awaited<ReturnType<typeof getNodeWorkspaces>>[number]["workspaces"][number]["pullState"],
22+
string
23+
> = {
24+
pulled: "Pulled",
25+
"in-progress": "Pulling",
26+
failed: "Pull Failed",
27+
"not-touched": "Image not modified",
28+
};
29+
export function UpdateDialog({ workspace, open, setOpen }: Props) {
1630
return (
1731
<Dialog open={open} onOpenChange={setOpen}>
1832
<DialogContent>
@@ -22,6 +36,7 @@ export function UpdateDialog({
2236
</DialogTitle>
2337
</DialogHeader>
2438
<form action={updateWorkspace} className="flex flex-col gap-2 w-full">
39+
<input hidden readOnly value={workspace?.dockerImage} name="dockerImage" />
2540
<Label htmlFor="name">Name</Label>
2641
<Input
2742
defaultValue={workspace.friendlyName}
@@ -39,7 +54,6 @@ export function UpdateDialog({
3954
name="category"
4055
required
4156
/>
42-
<input hidden readOnly value={workspace?.dockerImage} name="dockerImage" />
4357
<Label htmlFor="icon">Icon</Label>
4458
<Input defaultValue={workspace?.icon} id="icon" placeholder="Icon URL" name="icon" required />
4559
<SubmitButton>Save</SubmitButton>
@@ -48,3 +62,71 @@ export function UpdateDialog({
4862
</Dialog>
4963
);
5064
}
65+
export function NodeDialog({ workspace, open, setOpen }: Props) {
66+
const { data: nodeWorkspaces } = useSWR<Awaited<ReturnType<typeof getNodeWorkspaces>>>(
67+
"/api/admin/workspaces/node-workspaces",
68+
fetcher,
69+
{ refreshInterval: 1000 },
70+
);
71+
return (
72+
<Dialog open={open} onOpenChange={setOpen}>
73+
<DialogContent>
74+
<DialogHeader>
75+
<DialogTitle>
76+
Manage nodes for {workspace.friendlyName} ({workspace.dockerImage})
77+
</DialogTitle>
78+
</DialogHeader>
79+
<div className="flex flex-col gap-2 w-full">
80+
{nodeWorkspaces?.map((nodeWorkspace) => {
81+
const pulled = nodeWorkspace.workspaces.map((w) => w.image).includes(workspace.dockerImage);
82+
return (
83+
<Card key={nodeWorkspace.id}>
84+
<CardHeader className="-mb-4">
85+
<CardTitle className="text-xl">{nodeWorkspace.id}</CardTitle>
86+
<CardDescription>{pulled ? "Pulled" : "Not pulled"}</CardDescription>
87+
</CardHeader>
88+
{/* broken */}
89+
<CardContent className="flex flex-col gap-2">
90+
Pull Status:{" "}
91+
{
92+
statesMap[
93+
nodeWorkspace.workspaces.find((w) => w.image === workspace.dockerImage)?.pullState ||
94+
"not-touched"
95+
]
96+
}
97+
{pulled ? (
98+
<Button
99+
variant="destructive"
100+
onClick={() =>
101+
toast.promise(() => deleteImageFromNode(workspace, nodeWorkspace.id), {
102+
loading: "Removing from node",
103+
success: "Removed from node",
104+
error: "Failed to remove from node",
105+
})
106+
}
107+
>
108+
Remove from node
109+
</Button>
110+
) : (
111+
<Button
112+
variant="default"
113+
onClick={() =>
114+
toast.promise(() => pullOnNode(workspace, nodeWorkspace.id), {
115+
loading: "Requesting image pull on node",
116+
success: "Pull requested on node",
117+
error: "Failed to pull on node",
118+
})
119+
}
120+
>
121+
Add to node
122+
</Button>
123+
)}
124+
</CardContent>
125+
</Card>
126+
);
127+
})}
128+
</div>
129+
</DialogContent>
130+
</Dialog>
131+
);
132+
}

apps/web/src/app/(main)/admin/workspaces/page.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DataTable } from "@/components/ui/data-table";
2+
import { getNodeWorkspaces } from "@/lib/workspaces";
23
import { stardustConnector } from "@stardust/common/daemon/client";
34
import { getConfig } from "@stardust/config";
45
import db from "@stardust/db";
@@ -13,25 +14,18 @@ export default async function AdminPage() {
1314
session: true,
1415
},
1516
});
16-
const nodeMetadata = await Promise.all(
17-
getConfig().nodes.map(async (n) => {
18-
const { data } = await stardustConnector(n).workspaces.index.get();
19-
if (!data) throw new Error("No node data");
20-
return {
21-
id: n.id,
22-
workspaces: data.workspaces.map((w) => w.RepoTags[0].split(":")[0]),
23-
};
24-
}),
25-
);
17+
const nodeMetadata = await getNodeWorkspaces();
2618
const data = await Promise.all(
2719
dbData.map(async (d) => ({
28-
nodes: nodeMetadata.filter(({ workspaces }) => workspaces.includes(d.dockerImage)).map(({ id }) => id),
20+
nodes: nodeMetadata
21+
.filter(({ workspaces }) => workspaces.map((w) => w.image).includes(d.dockerImage))
22+
.map(({ id }) => id),
2923
...d,
3024
})),
3125
);
3226
return (
3327
<div className="flex h-full flex-col">
34-
<h1 className="py-6 text-3xl font-bold">Images</h1>
28+
<h1 className="py-6 text-3xl font-bold">Workspaces</h1>
3529
<section className="-ml-8">
3630
<DataTable data={data} columns={columns} />
3731
</section>

apps/web/src/app/(main)/page.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CardTitle } from "@/components/ui/card";
22
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
3-
import { stardustConnector } from "@stardust/common/daemon/client";
4-
import { getConfig } from "@stardust/config";
3+
import { getNodeWorkspaces } from "@/lib/workspaces";
54
import db, { workspace } from "@stardust/db";
65
import { Loader2 } from "lucide-react";
76
import Image from "next/image";
@@ -10,16 +9,7 @@ import { CreateForm } from "./page.client";
109

1110
export default async function Dashboard() {
1211
const workspaces = await db.select().from(workspace);
13-
const nodeMetadata = await Promise.all(
14-
getConfig().nodes.map(async (n) => {
15-
const { data } = await stardustConnector(n).workspaces.index.get();
16-
if (!data) throw new Error("No node data");
17-
return {
18-
id: n.id,
19-
workspaces: data.workspaces.map((w) => w.RepoTags[0].split(":")[0]),
20-
};
21-
}),
22-
);
12+
const nodeMetadata = await getNodeWorkspaces();
2313
return (
2414
<div className="m-auto flex w-full flex-col p-4">
2515
<h1 className="text-3xl font-bold mb-6">Workspaces</h1>
@@ -46,7 +36,7 @@ export default async function Dashboard() {
4636
<CreateForm
4737
workspace={workspace}
4838
nodeIds={nodeMetadata
49-
.filter(({ workspaces }) => workspaces.includes(workspace.dockerImage))
39+
.filter(({ workspaces }) => workspaces.map((w) => w.image).includes(workspace.dockerImage))
5040
.map((w) => w.id)}
5141
/>
5242
</Dialog>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { check } from "@/lib/admin-check";
2+
import { getNodeWorkspaces } from "@/lib/workspaces";
3+
4+
export async function GET() {
5+
await check();
6+
const data = await getNodeWorkspaces();
7+
return Response.json(data);
8+
}

apps/web/src/lib/workspaces.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use server";
2+
3+
import { stardustConnector } from "@stardust/common/daemon/client";
4+
import { getConfig } from "@stardust/config";
5+
6+
export async function getNodeWorkspaces() {
7+
return Promise.all(
8+
getConfig().nodes.map(async (n) => {
9+
const connector = stardustConnector(n);
10+
const { data } = await connector.workspaces.index.get();
11+
12+
if (!data) throw new Error("No node data");
13+
return {
14+
id: n.id,
15+
workspaces: await Promise.all(
16+
data.workspaces.map(async (w) => {
17+
const image = w.RepoTags[0].split(":")[0];
18+
const { data, error } = await connector.workspaces.create.get({
19+
query: {
20+
image,
21+
},
22+
});
23+
if (error) throw new Error(error.value.message);
24+
return {
25+
image,
26+
pullState: data.status,
27+
};
28+
}),
29+
),
30+
};
31+
}),
32+
);
33+
}

0 commit comments

Comments
 (0)