add join clan page

This commit is contained in:
Johannes Kirschbauer
2023-09-16 16:21:33 +02:00
parent 447d071ea3
commit 1bcff16b66
10 changed files with 299 additions and 9 deletions

View File

@@ -1,13 +1,24 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from .assets import asset_path from .assets import asset_path
from .routers import health, machines, root, vms from .routers import health, machines, root, vms
origins = [
"http://localhost:3000",
]
def setup_app() -> FastAPI: def setup_app() -> FastAPI:
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(health.router)
app.include_router(machines.router) app.include_router(machines.router)
app.include_router(root.router) app.include_router(root.router)

View File

@@ -55,17 +55,25 @@ def start_server(args: argparse.Namespace) -> None:
with ExitStack() as stack: with ExitStack() as stack:
headers: list[tuple[str, str]] = [] headers: list[tuple[str, str]] = []
if args.dev: 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}" open_url = f"http://{args.dev_host}:{args.dev_port}"
host = args.dev_host host = args.dev_host
if ":" in host: if ":" in host:
host = f"[{host}]" host = f"[{host}]"
headers = [ headers = [
( # (
"Access-Control-Allow-Origin", # "Access-Control-Allow-Origin",
f"http://{host}:{args.dev_port}", # 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: else:
open_url = f"http://[{args.host}]:{args.port}" open_url = f"http://[{args.host}]:{args.port}"

View File

@@ -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 # this placeholder is replaced by the path to nixpkgs
inputs.clan-core.url = "__CLAN_CORE__"; inputs.clan-core.url = "__CLAN_CORE__";

View File

@@ -10986,6 +10986,11 @@
descriptor = "^0.4.1"; descriptor = "^0.4.1";
pin = "0.4.1"; pin = "0.4.1";
}; };
pretty-bytes = {
descriptor = "^6.1.1";
pin = "6.1.1";
runtime = true;
};
react = { react = {
descriptor = "18.2.0"; descriptor = "18.2.0";
pin = "18.2.0"; pin = "18.2.0";
@@ -13086,6 +13091,9 @@
dev = true; dev = true;
key = "prettier-plugin-tailwindcss/0.4.1"; key = "prettier-plugin-tailwindcss/0.4.1";
}; };
"node_modules/pretty-bytes" = {
key = "pretty-bytes/6.1.1";
};
"node_modules/printable-characters" = { "node_modules/printable-characters" = {
dev = true; dev = true;
key = "printable-characters/1.0.42"; key = "printable-characters/1.0.42";
@@ -15195,6 +15203,19 @@
version = "0.4.1"; 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 = { printable-characters = {
"1.0.42" = { "1.0.42" = {
fetchInfo = { fetchInfo = {
@@ -18237,4 +18258,4 @@
}; };
}; };
}; };
} }

View File

@@ -1,5 +1,5 @@
const config = { const config = {
petstore: { clan: {
output: { output: {
mode: "tags-split", mode: "tags-split",
target: "src/api", target: "src/api",

View File

@@ -22,6 +22,7 @@
"hex-rgb": "^5.0.0", "hex-rgb": "^5.0.0",
"next": "13.4.12", "next": "13.4.12",
"postcss": "8.4.27", "postcss": "8.4.27",
"pretty-bytes": "^6.1.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.45.4", "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": { "node_modules/printable-characters": {
"version": "1.0.42", "version": "1.0.42",
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",

View File

@@ -26,6 +26,7 @@
"hex-rgb": "^5.0.0", "hex-rgb": "^5.0.0",
"next": "13.4.12", "next": "13.4.12",
"postcss": "8.4.27", "postcss": "8.4.27",
"pretty-bytes": "^6.1.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.45.4", "react-hook-form": "^7.45.4",

View File

@@ -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) => (
<Chip
color="secondary"
label={`${props.flakeUrl}#${props.flakeAttr}`}
sx={{ p: 2 }}
/>
);
interface VmPropLabelProps {
children: React.ReactNode;
}
const VmPropLabel = (props: VmPropLabelProps) => (
<div className="col-span-4 flex items-center sm:col-span-1">
{props.children}
</div>
);
interface VmPropContentProps {
children: React.ReactNode;
}
const VmPropContent = (props: VmPropContentProps) => (
<div className="col-span-4 font-bold sm:col-span-3">{props.children}</div>
);
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 (
<div className="grid grid-cols-4 gap-y-10">
<div className="col-span-4">
<ListSubheader>General</ListSubheader>
</div>
<VmPropLabel>Flake</VmPropLabel>
<VmPropContent>
<FlakeBadge flakeAttr={flake_attr} flakeUrl={flake_url} />
</VmPropContent>
<VmPropLabel>Machine</VmPropLabel>
<VmPropContent>{flake_attr}</VmPropContent>
<div className="col-span-4">
<ListSubheader>VM</ListSubheader>
</div>
<VmPropLabel>CPU Cores</VmPropLabel>
<VmPropContent>
<Numbers fontSize="inherit" />
<span className="font-bold text-black">{cores}</span>
</VmPropContent>
<VmPropLabel>Graphics</VmPropLabel>
<VmPropContent>
<Switch checked={graphics} />
</VmPropContent>
<VmPropLabel>Memory Size</VmPropLabel>
<VmPropContent>{prettyBytes(memory_size * 1024 * 1024)}</VmPropContent>
<div className="col-span-4 grid items-center">
{isStarting && <LinearProgress />}
<Button
disabled={isStarting}
variant="contained"
onClick={handleStartVm}
>
Spin up VM
</Button>
</div>
</div>
);
};
interface ErrorLogOptions {
lines: string[];
}
const ErrorLog = (props: ErrorLogOptions) => {
const { lines } = props;
return (
<div className="w-full bg-slate-800 p-4 text-white shadow-inner shadow-black">
<div className="mb-1 text-slate-400">Log</div>
{lines.map((item, idx) => (
<span key={`${idx}`} className="mb-2 block break-words">
{item}
<br />
</span>
))}
</div>
);
};
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 (
<div className="grid h-[70vh] w-full place-items-center gap-y-4">
<Typography variant="h4" className="w-full text-center">
Join{" "}
<Typography variant="h4" className="font-bold" component={"span"}>
{clanName}
</Typography>
{"' "}
Clan
</Typography>
{error && (
<Alert severity="error" className="w-full max-w-xl">
<AlertTitle>Error</AlertTitle>
An Error occurred - See details below
</Alert>
)}
<div className="w-full max-w-xl">
{isLoading && (
<div className="w-full">
<Typography variant="subtitle2">Loading Flake</Typography>
<LinearProgress className="mb-2 w-full" />
<div className="grid w-full place-items-center">
<FlakeBadge flakeUrl={flakeUrl} flakeAttr={flakeAttribute} />
</div>
<Typography variant="subtitle1"></Typography>
</div>
)}
{(!flakeUrl || !flakeAttribute) && <div>Invalid URL</div>}
{config && <VmDetails vmConfig={config} />}
{error && (
<ErrorLog
lines={
error?.response?.data?.detail
?.map((err, idx) => err.msg.split("\n"))
?.flat()
.filter(Boolean) || []
}
/>
)}
</div>
</div>
);
}

View File

@@ -54,7 +54,7 @@ export function CustomConfig(props: FormStepContentProps) {
} }
return acc; return acc;
}, {}), }, {}),
[schema], [schema]
); );
return isLoading ? ( return isLoading ? (
@@ -124,7 +124,7 @@ function PureCustomConfig(props: PureCustomConfigProps) {
message: "invalid config", message: "invalid config",
}); });
toast.error( toast.error(
"Configuration is invalid. Please check the highlighted fields for details.", "Configuration is invalid. Please check the highlighted fields for details."
); );
} else { } else {
formHooks.clearErrors("config"); formHooks.clearErrors("config");

View File

@@ -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<VmConfig>();
const [error, setError] = useState<AxiosError<HTTPValidationError>>();
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<HTTPValidationError>;
setError(err);
toast.error(err.message);
return undefined;
} finally {
setIsLoading(false);
}
};
getVmInfo(url, attr).then((c) => setConfig(c));
}, [url, attr]);
return {
error,
isLoading,
config,
};
};