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}
+          
+        ))}
+      
+
+ ); +};