add join clan page
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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__";
|
||||||
|
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
petstore: {
|
clan: {
|
||||||
output: {
|
output: {
|
||||||
mode: "tags-split",
|
mode: "tags-split",
|
||||||
target: "src/api",
|
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",
|
"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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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;
|
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");
|
||||||
|
|||||||
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