From 1bcff16b664cb67fced4fc3cee84e47adc502fbb Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 16 Sep 2023 16:21:33 +0200 Subject: [PATCH] add join clan page --- pkgs/clan-cli/clan_cli/webui/app.py | 11 ++ pkgs/clan-cli/clan_cli/webui/server.py | 18 +- .../tests/test_flake_with_core/flake.nix | 3 + pkgs/ui/nix/pdefs.nix | 23 ++- pkgs/ui/orval.config.ts | 2 +- pkgs/ui/package-lock.json | 12 ++ pkgs/ui/package.json | 1 + pkgs/ui/src/app/join/page.tsx | 184 ++++++++++++++++++ .../createMachineForm/customConfig.tsx | 4 +- pkgs/ui/src/components/hooks/useVms.tsx | 50 +++++ 10 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 pkgs/ui/src/app/join/page.tsx create mode 100644 pkgs/ui/src/components/hooks/useVms.tsx diff --git a/pkgs/clan-cli/clan_cli/webui/app.py b/pkgs/clan-cli/clan_cli/webui/app.py index 76f5809f5..e7469d55a 100644 --- a/pkgs/clan-cli/clan_cli/webui/app.py +++ b/pkgs/clan-cli/clan_cli/webui/app.py @@ -1,13 +1,24 @@ from fastapi import FastAPI from fastapi.routing import APIRoute from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware from .assets import asset_path from .routers import health, machines, root, vms +origins = [ + "http://localhost:3000", +] def setup_app() -> FastAPI: app = FastAPI() + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) app.include_router(health.router) app.include_router(machines.router) app.include_router(root.router) diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index 7d915a743..213b2768a 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -55,17 +55,25 @@ def start_server(args: argparse.Namespace) -> None: with ExitStack() as stack: headers: list[tuple[str, str]] = [] if args.dev: - stack.enter_context(spawn_node_dev_server(args.dev_host, args.dev_port)) + # stack.enter_context(spawn_node_dev_server(args.dev_host, args.dev_port)) open_url = f"http://{args.dev_host}:{args.dev_port}" host = args.dev_host if ":" in host: host = f"[{host}]" headers = [ - ( - "Access-Control-Allow-Origin", - f"http://{host}:{args.dev_port}", - ) + # ( + # "Access-Control-Allow-Origin", + # f"http://{host}:{args.dev_port}", + # ), + # ( + # "Access-Control-Allow-Methods", + # "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" + # ), + # ( + # "Allow", + # "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" + # ) ] else: open_url = f"http://[{args.host}]:{args.port}" diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index 0a36a1332..fab76b5b1 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -1,4 +1,7 @@ { + # Use this path to our repo root e.g. for UI test + # inputs.clan-core.url = "../../../../."; + # this placeholder is replaced by the path to nixpkgs inputs.clan-core.url = "__CLAN_CORE__"; diff --git a/pkgs/ui/nix/pdefs.nix b/pkgs/ui/nix/pdefs.nix index 5224db1ff..4e79bc93a 100644 --- a/pkgs/ui/nix/pdefs.nix +++ b/pkgs/ui/nix/pdefs.nix @@ -10986,6 +10986,11 @@ descriptor = "^0.4.1"; pin = "0.4.1"; }; + pretty-bytes = { + descriptor = "^6.1.1"; + pin = "6.1.1"; + runtime = true; + }; react = { descriptor = "18.2.0"; pin = "18.2.0"; @@ -13086,6 +13091,9 @@ dev = true; key = "prettier-plugin-tailwindcss/0.4.1"; }; + "node_modules/pretty-bytes" = { + key = "pretty-bytes/6.1.1"; + }; "node_modules/printable-characters" = { dev = true; key = "printable-characters/1.0.42"; @@ -15195,6 +15203,19 @@ version = "0.4.1"; }; }; + pretty-bytes = { + "6.1.1" = { + fetchInfo = { + narHash = "sha256-ERXqMD/9tkPebbHVL3n/9EQRz7mFs5VYO6k/wo5JDzQ="; + type = "tarball"; + url = "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz"; + }; + ident = "pretty-bytes"; + ltype = "file"; + treeInfo = { }; + version = "6.1.1"; + }; + }; printable-characters = { "1.0.42" = { fetchInfo = { @@ -18237,4 +18258,4 @@ }; }; }; -} +} \ No newline at end of file diff --git a/pkgs/ui/orval.config.ts b/pkgs/ui/orval.config.ts index db48112ce..b92bd3d2e 100644 --- a/pkgs/ui/orval.config.ts +++ b/pkgs/ui/orval.config.ts @@ -1,5 +1,5 @@ const config = { - petstore: { + clan: { output: { mode: "tags-split", target: "src/api", diff --git a/pkgs/ui/package-lock.json b/pkgs/ui/package-lock.json index 5462475ba..fc249db2b 100644 --- a/pkgs/ui/package-lock.json +++ b/pkgs/ui/package-lock.json @@ -22,6 +22,7 @@ "hex-rgb": "^5.0.0", "next": "13.4.12", "postcss": "8.4.27", + "pretty-bytes": "^6.1.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.45.4", @@ -6810,6 +6811,17 @@ } } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", diff --git a/pkgs/ui/package.json b/pkgs/ui/package.json index 6ae1b83ee..02e28cdce 100644 --- a/pkgs/ui/package.json +++ b/pkgs/ui/package.json @@ -26,6 +26,7 @@ "hex-rgb": "^5.0.0", "next": "13.4.12", "postcss": "8.4.27", + "pretty-bytes": "^6.1.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.45.4", diff --git a/pkgs/ui/src/app/join/page.tsx b/pkgs/ui/src/app/join/page.tsx new file mode 100644 index 000000000..660c407ae --- /dev/null +++ b/pkgs/ui/src/app/join/page.tsx @@ -0,0 +1,184 @@ +"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"; + +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} +
+
+ ))} +
+ ); +}; + +export default function Page() { + const queryParams = useSearchParams(); + const flakeUrl = queryParams.get("flake") || ""; + const flakeAttribute = queryParams.get("attr") || "default"; + + const { config, error, isLoading } = useVms({ + url: flakeUrl, + attr: flakeAttribute, + }); + const clanName = "Lassul.us"; + return ( +
+ + Join{" "} + + {clanName} + + {"' "} + Clan + + {error && ( + + Error + An Error occurred - See details below + + )} +
+ {isLoading && ( +
+ Loading Flake + +
+ +
+ + +
+ )} + {(!flakeUrl || !flakeAttribute) &&
Invalid URL
} + {config && } + {error && ( + err.msg.split("\n")) + ?.flat() + .filter(Boolean) || [] + } + /> + )} +
+
+ ); +} diff --git a/pkgs/ui/src/components/createMachineForm/customConfig.tsx b/pkgs/ui/src/components/createMachineForm/customConfig.tsx index fd005fbf3..d7805ec2a 100644 --- a/pkgs/ui/src/components/createMachineForm/customConfig.tsx +++ b/pkgs/ui/src/components/createMachineForm/customConfig.tsx @@ -54,7 +54,7 @@ export function CustomConfig(props: FormStepContentProps) { } return acc; }, {}), - [schema], + [schema] ); return isLoading ? ( @@ -124,7 +124,7 @@ function PureCustomConfig(props: PureCustomConfigProps) { message: "invalid config", }); toast.error( - "Configuration is invalid. Please check the highlighted fields for details.", + "Configuration is invalid. Please check the highlighted fields for details." ); } else { formHooks.clearErrors("config"); diff --git a/pkgs/ui/src/components/hooks/useVms.tsx b/pkgs/ui/src/components/hooks/useVms.tsx new file mode 100644 index 000000000..3901a256f --- /dev/null +++ b/pkgs/ui/src/components/hooks/useVms.tsx @@ -0,0 +1,50 @@ +import { inspectVm } from "@/api/default/default"; +import { HTTPValidationError, VmConfig } from "@/api/model"; +import { AxiosError } from "axios"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; + +interface UseVmsOptions { + url: string; + attr: string; +} +export const useVms = (options: UseVmsOptions) => { + const { url, attr } = options; + const [isLoading, setIsLoading] = useState(true); + const [config, setConfig] = useState(); + const [error, setError] = useState>(); + + useEffect(() => { + const getVmInfo = async (url: string, attr: string) => { + if (url === "") { + toast.error("Flake url is missing", { id: "missing.flake.url" }); + return undefined; + } + try { + const response = await inspectVm({ + flake_attr: attr, + flake_url: url, + }); + const { + data: { config }, + } = response; + setError(undefined); + return config; + } catch (e) { + const err = e as AxiosError; + setError(err); + toast.error(err.message); + return undefined; + } finally { + setIsLoading(false); + } + }; + getVmInfo(url, attr).then((c) => setConfig(c)); + }, [url, attr]); + + return { + error, + isLoading, + config, + }; +};