add join clan page
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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__";
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const config = {
|
||||
petstore: {
|
||||
clan: {
|
||||
output: {
|
||||
mode: "tags-split",
|
||||
target: "src/api",
|
||||
|
||||
12
pkgs/ui/package-lock.json
generated
12
pkgs/ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
184
pkgs/ui/src/app/join/page.tsx
Normal file
184
pkgs/ui/src/app/join/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
50
pkgs/ui/src/components/hooks/useVms.tsx
Normal file
50
pkgs/ui/src/components/hooks/useVms.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user