diff --git a/pkgs/clan-cli/clan_cli/webui/app.py b/pkgs/clan-cli/clan_cli/webui/app.py
index 834f337f7..8ac149d9e 100644
--- a/pkgs/clan-cli/clan_cli/webui/app.py
+++ b/pkgs/clan-cli/clan_cli/webui/app.py
@@ -4,7 +4,7 @@ from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from .assets import asset_path
-from .routers import health, machines, root, vms
+from .routers import health, machines, root, vms, flake
origins = [
"http://localhost:3000",
@@ -20,6 +20,7 @@ def setup_app() -> FastAPI:
allow_methods=["*"],
allow_headers=["*"],
)
+ app.include_router(flake.router)
app.include_router(health.router)
app.include_router(machines.router)
app.include_router(root.router)
diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py
new file mode 100644
index 000000000..c4e4ad60a
--- /dev/null
+++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py
@@ -0,0 +1,50 @@
+import asyncio
+import json
+from fastapi import APIRouter, HTTPException, status
+from pathlib import Path
+from clan_cli.webui.schemas import FlakeAction, FlakeResponse
+
+from ...nix import nix_build, nix_eval, nix_command
+
+router = APIRouter()
+
+@router.get("/api/flake")
+async def inspect_flake(
+ url: str,
+) -> FlakeResponse:
+ actions = []
+ # Extract the flake from the given URL
+ # We do this by running 'nix flake prefetch {url} --json'
+ cmd = nix_command([
+ "flake",
+ "prefetch",
+ url,
+ "--json"
+ ])
+ proc = await asyncio.create_subprocess_exec(
+ cmd[0],
+ *cmd[1:],
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+ stdout, stderr = await proc.communicate()
+
+ if proc.returncode != 0:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=str(stderr))
+
+
+ data: dict[str,str] = json.loads(stdout)
+
+ if data.get("storePath") is None:
+ raise HTTPException(status_code=500,detail="Could not load flake")
+
+ content: str
+ with open(Path(data.get("storePath", "")) / Path("flake.nix")) as f:
+ content = f.read()
+
+
+ # TODO: Figure out some measure when it is insecure to inspect or create a VM
+ actions.append(FlakeAction(id="vms/inspect", uri = f"api/vms/inspect"))
+ actions.append(FlakeAction(id="vms/create", uri = f"api/vms/create"))
+
+ return FlakeResponse(content=content, actions=actions )
diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py
index dc6ea3f53..28087bd36 100644
--- a/pkgs/clan-cli/clan_cli/webui/schemas.py
+++ b/pkgs/clan-cli/clan_cli/webui/schemas.py
@@ -1,7 +1,7 @@
from enum import Enum
from pydantic import BaseModel, Field
-
+from typing import List
class Status(Enum):
ONLINE = "online"
@@ -45,3 +45,12 @@ class VmConfig(BaseModel):
class VmInspectResponse(BaseModel):
config: VmConfig
+
+
+class FlakeAction(BaseModel):
+ id: str
+ uri: str
+
+class FlakeResponse(BaseModel):
+ content: str
+ actions: List[FlakeAction]
diff --git a/pkgs/ui/pconf.cjs b/pkgs/ui/pconf.cjs
deleted file mode 100644
index f72fee6f8..000000000
--- a/pkgs/ui/pconf.cjs
+++ /dev/null
@@ -1,5 +0,0 @@
-// prettier.config.js
-module.exports = {
- plugins: ["prettier-plugin-tailwindcss"],
- tailwindFunctions: ["clsx", "tw"],
-};
diff --git a/pkgs/ui/src/app/join/page.tsx b/pkgs/ui/src/app/join/page.tsx
index 660c407ae..81bfc126f 100644
--- a/pkgs/ui/src/app/join/page.tsx
+++ b/pkgs/ui/src/app/join/page.tsx
@@ -1,143 +1,24 @@
"use client";
import React, { useState } from "react";
-import { VmConfig } from "@/api/model";
-import { useVms } from "@/components/hooks/useVms";
-import prettyBytes from "pretty-bytes";
-
-import {
- Alert,
- AlertTitle,
- Button,
- Chip,
- LinearProgress,
- ListSubheader,
- Switch,
- Typography,
-} from "@mui/material";
+import { Button, Paper, Typography } from "@mui/material";
import { useSearchParams } from "next/navigation";
-import { toast } from "react-hot-toast";
-import { Error, Numbers } from "@mui/icons-material";
-import { createVm, inspectVm } from "@/api/default/default";
-
-interface FlakeBadgeProps {
- flakeUrl: string;
- flakeAttr: string;
-}
-const FlakeBadge = (props: FlakeBadgeProps) => (
-
-);
-
-interface VmPropLabelProps {
- children: React.ReactNode;
-}
-const VmPropLabel = (props: VmPropLabelProps) => (
-
- {props.children}
-
-);
-
-interface VmPropContentProps {
- children: React.ReactNode;
-}
-const VmPropContent = (props: VmPropContentProps) => (
- {props.children}
-);
-
-interface VmDetailsProps {
- vmConfig: VmConfig;
-}
-
-const VmDetails = (props: VmDetailsProps) => {
- const { vmConfig } = props;
- const { cores, flake_attr, flake_url, graphics, memory_size } = vmConfig;
- const [isStarting, setStarting] = useState(false);
- const handleStartVm = async () => {
- setStarting(true);
- const response = await createVm(vmConfig);
- setStarting(false);
- if (response.statusText === "OK") {
- toast.success(("VM created @ " + response?.data) as string);
- } else {
- toast.error("Could not create VM");
- }
- };
- return (
-
-
- General
-
-
-
Flake
-
-
-
-
-
Machine
-
{flake_attr}
-
-
- VM
-
-
CPU Cores
-
-
- {cores}
-
-
-
Graphics
-
-
-
-
-
Memory Size
-
{prettyBytes(memory_size * 1024 * 1024)}
-
-
- {isStarting && }
-
-
-
- );
-};
-
-interface ErrorLogOptions {
- lines: string[];
-}
-const ErrorLog = (props: ErrorLogOptions) => {
- const { lines } = props;
- return (
-
-
Log
- {lines.map((item, idx) => (
-
- {item}
-
-
- ))}
-
- );
-};
+import GppMaybeIcon from "@mui/icons-material/GppMaybe";
+import { useInspectFlake } from "@/api/default/default";
+import { ConfirmVM } from "@/components/join/join";
+import { LoadingOverlay } from "@/components/join/loadingOverlay";
+import { FlakeBadge } from "@/components/flakeBadge/flakeBadge";
+import { Log } from "@/components/join/log";
export default function Page() {
const queryParams = useSearchParams();
const flakeUrl = queryParams.get("flake") || "";
const flakeAttribute = queryParams.get("attr") || "default";
+ const [userConfirmed, setUserConfirmed] = useState(false);
- const { config, error, isLoading } = useVms({
- url: flakeUrl,
- attr: flakeAttribute,
- });
const clanName = "Lassul.us";
+
+ const { data, error, isLoading } = useInspectFlake({ url: flakeUrl });
+
return (
@@ -148,37 +29,46 @@ export default function Page() {
{"' "}
Clan
- {error && (
-
- Error
- An Error occurred - See details below
-
- )}
-
- {isLoading && (
-
-
Loading Flake
-
-
-
-
-
+ {flakeUrl && flakeAttribute ? (
+ userConfirmed ? (
+
+ ) : (
+
+ {isLoading && (
+
+ }
+ />
+ )}
+ {data && (
+ <>
+
+ To build the VM you must trust the Author of this Flake
+
+
+
+
+ >
+ )}
- )}
- {(!flakeUrl || !flakeAttribute) &&
Invalid URL
}
- {config &&
}
- {error && (
-
err.msg.split("\n"))
- ?.flat()
- .filter(Boolean) || []
- }
- />
- )}
-
+ )
+ ) : (
+
Invalid URL
+ )}
);
}
diff --git a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx
new file mode 100644
index 000000000..1fcc345b9
--- /dev/null
+++ b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx
@@ -0,0 +1,13 @@
+import { Chip } from "@mui/material";
+
+interface FlakeBadgeProps {
+ flakeUrl: string;
+ flakeAttr: string;
+}
+export const FlakeBadge = (props: FlakeBadgeProps) => (
+
+);
diff --git a/pkgs/ui/src/components/join/join.tsx b/pkgs/ui/src/components/join/join.tsx
new file mode 100644
index 000000000..5c1c60ee4
--- /dev/null
+++ b/pkgs/ui/src/components/join/join.tsx
@@ -0,0 +1,147 @@
+"use client";
+import React, { useState } from "react";
+import { VmConfig } from "@/api/model";
+import { useVms } from "@/components/hooks/useVms";
+import prettyBytes from "pretty-bytes";
+
+import {
+ Alert,
+ AlertTitle,
+ Button,
+ Chip,
+ LinearProgress,
+ ListSubheader,
+ Switch,
+ Typography,
+} from "@mui/material";
+import { useSearchParams } from "next/navigation";
+import { toast } from "react-hot-toast";
+import { Error, Numbers } from "@mui/icons-material";
+import { createVm, inspectVm } from "@/api/default/default";
+import { LoadingOverlay } from "./loadingOverlay";
+import { FlakeBadge } from "../flakeBadge/flakeBadge";
+import { Log } from "./log";
+
+interface VmPropLabelProps {
+ children: React.ReactNode;
+}
+const VmPropLabel = (props: VmPropLabelProps) => (
+
+ {props.children}
+
+);
+
+interface VmPropContentProps {
+ children: React.ReactNode;
+}
+const VmPropContent = (props: VmPropContentProps) => (
+
{props.children}
+);
+
+interface VmDetailsProps {
+ vmConfig: VmConfig;
+}
+
+const VmDetails = (props: VmDetailsProps) => {
+ const { vmConfig } = props;
+ const { cores, flake_attr, flake_url, graphics, memory_size } = vmConfig;
+ const [isStarting, setStarting] = useState(false);
+ const handleStartVm = async () => {
+ setStarting(true);
+ const response = await createVm(vmConfig);
+ setStarting(false);
+ if (response.statusText === "OK") {
+ toast.success(("VM created @ " + response?.data) as string);
+ } else {
+ toast.error("Could not create VM");
+ }
+ };
+ return (
+
+
+ General
+
+
+
Flake
+
+
+
+
+
Machine
+
{flake_attr}
+
+
+ VM
+
+
CPU Cores
+
+
+ {cores}
+
+
+
Graphics
+
+
+
+
+
Memory Size
+
{prettyBytes(memory_size * 1024 * 1024)}
+
+
+ {isStarting && }
+
+
+
+ );
+};
+
+interface ConfirmVMProps {
+ url: string;
+ attr: string;
+ clanName: string;
+}
+
+export function ConfirmVM(props: ConfirmVMProps) {
+ const { url, attr, clanName } = props;
+
+ const { config, error, isLoading } = useVms({
+ url,
+ attr,
+ });
+ return (
+ <>
+ {error && (
+
+ Error
+ An Error occurred - See details below
+
+ )}
+
+ {isLoading && (
+ }
+ />
+ )}
+ {config && }
+ {error && (
+ err.msg.split("\n"))
+ ?.flat()
+ .filter(Boolean) || []
+ }
+ />
+ )}
+
+ >
+ );
+}
diff --git a/pkgs/ui/src/components/join/loadingOverlay.tsx b/pkgs/ui/src/components/join/loadingOverlay.tsx
new file mode 100644
index 000000000..43db2adff
--- /dev/null
+++ b/pkgs/ui/src/components/join/loadingOverlay.tsx
@@ -0,0 +1,17 @@
+import { LinearProgress, Typography } from "@mui/material";
+
+interface LoadingOverlayProps {
+ title: React.ReactNode;
+ subtitle: React.ReactNode;
+}
+export const LoadingOverlay = (props: LoadingOverlayProps) => {
+ const { title, subtitle } = props;
+ return (
+
+
{title}
+
+
{subtitle}
+
+
+ );
+};
diff --git a/pkgs/ui/src/components/join/log.tsx b/pkgs/ui/src/components/join/log.tsx
new file mode 100644
index 000000000..bb7b2f330
--- /dev/null
+++ b/pkgs/ui/src/components/join/log.tsx
@@ -0,0 +1,19 @@
+interface LogOptions {
+ lines: string[];
+ title?: string;
+}
+export const Log = (props: LogOptions) => {
+ const { lines, title } = props;
+ return (
+
+
{title}
+
+ {lines.map((item, idx) => (
+
+ {item}
+
+ ))}
+
+
+ );
+};