diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py
index ecfc1dbe7..bbb2158d9 100644
--- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py
+++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py
@@ -18,7 +18,7 @@ async def inspect_flake(
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"])
+ cmd = nix_command(["flake", "prefetch", url, "--json", "--refresh"])
proc = await asyncio.create_subprocess_exec(
cmd[0],
*cmd[1:],
diff --git a/pkgs/clan-cli/clan_cli/webui/routers/vms.py b/pkgs/clan-cli/clan_cli/webui/routers/vms.py
index 394eb8a63..0e77d5b18 100644
--- a/pkgs/clan-cli/clan_cli/webui/routers/vms.py
+++ b/pkgs/clan-cli/clan_cli/webui/routers/vms.py
@@ -115,7 +115,7 @@ command output:
@router.get("/api/vms/{uuid}/status")
-async def vm_status(uuid: UUID) -> VmStatusResponse:
+async def get_vm_status(uuid: UUID) -> VmStatusResponse:
task = get_task(uuid)
status: list[int | None] = list(map(lambda x: x.returncode, task.procs))
log.debug(msg=f"returncodes: {status}. task.finished: {task.finished}")
diff --git a/pkgs/ui/src/app/dashboard/page.tsx b/pkgs/ui/src/app/dashboard/page.tsx
new file mode 100644
index 000000000..c230c6a2c
--- /dev/null
+++ b/pkgs/ui/src/app/dashboard/page.tsx
@@ -0,0 +1,61 @@
+"use client";
+import { RecentActivity } from "@/components/dashboard/activity";
+import { AppOverview } from "@/components/dashboard/appOverview";
+import { NetworkOverview } from "@/components/dashboard/NetworkOverview";
+import { Notifications } from "@/components/dashboard/notifications";
+import { QuickActions } from "@/components/dashboard/quickActions";
+import { TaskQueue } from "@/components/dashboard/taskQueue";
+import { tw } from "@/utils/tailwind";
+
+interface DashboardCardProps {
+ children?: React.ReactNode;
+ rowSpan?: number;
+ sx?: string;
+}
+const DashboardCard = (props: DashboardCardProps) => {
+ const { children, rowSpan, sx = "" } = props;
+ return (
+
+ {children}
+
+ );
+};
+
+interface DashboardPanelProps {
+ children?: React.ReactNode;
+}
+const DashboardPanel = (props: DashboardPanelProps) => {
+ const { children } = props;
+ return (
+ {children}
+ );
+};
+
+export default function Dashboard() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/join/page.tsx b/pkgs/ui/src/app/join/page.tsx
deleted file mode 100644
index 81bfc126f..000000000
--- a/pkgs/ui/src/app/join/page.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-import React, { useState } from "react";
-import { Button, Paper, Typography } from "@mui/material";
-import { useSearchParams } from "next/navigation";
-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 clanName = "Lassul.us";
-
- const { data, error, isLoading } = useInspectFlake({ url: flakeUrl });
-
- return (
-
-
- Join{" "}
-
- {clanName}
-
- {"' "}
- Clan
-
-
- {flakeUrl && flakeAttribute ? (
- userConfirmed ? (
-
- ) : (
-
- {isLoading && (
-
- }
- />
- )}
- {data && (
- <>
-
- To build the VM you must trust the Author of this Flake
-
-
-
-
- >
- )}
-
- )
- ) : (
-
Invalid URL
- )}
-
- );
-}
diff --git a/pkgs/ui/src/app/page.tsx b/pkgs/ui/src/app/page.tsx
index 5500dfd3c..40b43ee71 100644
--- a/pkgs/ui/src/app/page.tsx
+++ b/pkgs/ui/src/app/page.tsx
@@ -1,60 +1,79 @@
-import { RecentActivity } from "@/components/dashboard/activity";
-import { AppOverview } from "@/components/dashboard/appOverview";
-import { NetworkOverview } from "@/components/dashboard/NetworkOverview";
-import { Notifications } from "@/components/dashboard/notifications";
-import { QuickActions } from "@/components/dashboard/quickActions";
-import { TaskQueue } from "@/components/dashboard/taskQueue";
-import { tw } from "@/utils/tailwind";
+"use client";
+import React, { useEffect, useState } from "react";
+import {
+ Button,
+ IconButton,
+ Input,
+ InputAdornment,
+ Paper,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useSearchParams } from "next/navigation";
+import { useInspectFlake } from "@/api/default/default";
+import { ConfirmVM } from "@/components/join/confirmVM";
+import { LoadingOverlay } from "@/components/join/loadingOverlay";
+import { FlakeBadge } from "@/components/flakeBadge/flakeBadge";
+import { Log } from "@/components/join/log";
-interface DashboardCardProps {
- children?: React.ReactNode;
- rowSpan?: number;
- sx?: string;
-}
-const DashboardCard = (props: DashboardCardProps) => {
- const { children, rowSpan, sx = "" } = props;
- return (
-
- {children}
-
- );
+import { useForm, SubmitHandler, Controller } from "react-hook-form";
+import { Confirm } from "@/components/join/confirm";
+import { Layout } from "@/components/join/layout";
+import { ChevronRight } from "@mui/icons-material";
+
+type FormValues = {
+ flakeUrl: string;
+ flakeAttribute: string;
};
-interface DashboardPanelProps {
- children?: React.ReactNode;
-}
-const DashboardPanel = (props: DashboardPanelProps) => {
- const { children } = props;
- return (
- {children}
- );
-};
+export default function Page() {
+ const queryParams = useSearchParams();
+ const flakeUrl = queryParams.get("flake") || "";
+ const flakeAttribute = queryParams.get("attr") || "default";
+ const { handleSubmit, control, formState, getValues, reset } =
+ useForm({ defaultValues: { flakeUrl: "" } });
+
+ const onSubmit: SubmitHandler = (data) => console.log(data);
-export default function Dashboard() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {!formState.isSubmitted && !flakeUrl && (
+
+ )}
+ {(formState.isSubmitted || flakeUrl) && (
+ reset()}
+ flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl}
+ />
+ )}
+
);
}
diff --git a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx
index 1fcc345b9..1b0b91a72 100644
--- a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx
+++ b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx
@@ -8,6 +8,11 @@ export const FlakeBadge = (props: FlakeBadgeProps) => (
);
diff --git a/pkgs/ui/src/components/hooks/useVms.tsx b/pkgs/ui/src/components/hooks/useVms.tsx
index 3901a256f..212d290ea 100644
--- a/pkgs/ui/src/components/hooks/useVms.tsx
+++ b/pkgs/ui/src/components/hooks/useVms.tsx
@@ -16,7 +16,7 @@ export const useVms = (options: UseVmsOptions) => {
useEffect(() => {
const getVmInfo = async (url: string, attr: string) => {
- if (url === "") {
+ if (url === "" || !url) {
toast.error("Flake url is missing", { id: "missing.flake.url" });
return undefined;
}
diff --git a/pkgs/ui/src/components/join/configureVM.tsx b/pkgs/ui/src/components/join/configureVM.tsx
new file mode 100644
index 000000000..e1541f619
--- /dev/null
+++ b/pkgs/ui/src/components/join/configureVM.tsx
@@ -0,0 +1,138 @@
+import {
+ Button,
+ InputAdornment,
+ LinearProgress,
+ ListSubheader,
+ MenuItem,
+ Select,
+ Switch,
+ TextField,
+} from "@mui/material";
+import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form";
+import { FlakeBadge } from "../flakeBadge/flakeBadge";
+import { createVm, useGetVmLogs } from "@/api/default/default";
+import { VmConfig } from "@/api/model";
+import { Dispatch, SetStateAction, useState } from "react";
+import { toast } from "react-hot-toast";
+
+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;
+ formHooks: UseFormReturn;
+ setVmUuid: Dispatch>;
+}
+
+export const ConfigureVM = (props: VmDetailsProps) => {
+ const { vmConfig, formHooks, setVmUuid } = props;
+ const { control, handleSubmit } = formHooks;
+ const { cores, flake_attr, flake_url, graphics, memory_size } = vmConfig;
+ const [isStarting, setStarting] = useState(false);
+
+ const onSubmit: SubmitHandler = async (data) => {
+ setStarting(true);
+ console.log(data);
+ const response = await createVm(data);
+ const { uuid } = response?.data || null;
+
+ setVmUuid(() => uuid);
+ setStarting(false);
+ if (response.statusText === "OK") {
+ toast.success(("Joined @ " + uuid) as string);
+ } else {
+ toast.error("Could not join");
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/pkgs/ui/src/components/join/confirm.tsx b/pkgs/ui/src/components/join/confirm.tsx
new file mode 100644
index 000000000..cf04bdfe3
--- /dev/null
+++ b/pkgs/ui/src/components/join/confirm.tsx
@@ -0,0 +1,56 @@
+"use client";
+import { useState } from "react";
+import { LoadingOverlay } from "./loadingOverlay";
+import { FlakeBadge } from "../flakeBadge/flakeBadge";
+import { Typography, Button } from "@mui/material";
+import { FlakeResponse } from "@/api/model";
+import { ConfirmVM } from "./confirmVM";
+import { Log } from "./log";
+import GppMaybeIcon from "@mui/icons-material/GppMaybe";
+import { useInspectFlake } from "@/api/default/default";
+
+interface ConfirmProps {
+ flakeUrl: string;
+ handleBack: () => void;
+}
+export const Confirm = (props: ConfirmProps) => {
+ const { flakeUrl, handleBack } = props;
+ const [userConfirmed, setUserConfirmed] = useState(false);
+
+ const { data, error, isLoading } = useInspectFlake({ url: flakeUrl });
+
+ return userConfirmed ? (
+
+ ) : (
+
+ {isLoading && (
+ }
+ />
+ )}
+ {data && (
+ <>
+
+ To join the clan you must trust the Author
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/pkgs/ui/src/components/join/confirmVM.tsx b/pkgs/ui/src/components/join/confirmVM.tsx
new file mode 100644
index 000000000..efad07ea0
--- /dev/null
+++ b/pkgs/ui/src/components/join/confirmVM.tsx
@@ -0,0 +1,104 @@
+"use client";
+import React, { useEffect, useState } from "react";
+import { VmConfig } from "@/api/model";
+import { useVms } from "@/components/hooks/useVms";
+
+import { Alert, AlertTitle, Button } from "@mui/material";
+
+import { useSearchParams } from "next/navigation";
+
+import { createVm, inspectVm, useGetVmLogs } from "@/api/default/default";
+
+import { LoadingOverlay } from "./loadingOverlay";
+import { FlakeBadge } from "../flakeBadge/flakeBadge";
+import { Log } from "./log";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { ConfigureVM } from "./configureVM";
+import { VmBuildLogs } from "./vmBuildLogs";
+
+interface ConfirmVMProps {
+ url: string;
+ handleBack: () => void;
+}
+
+export function ConfirmVM(props: ConfirmVMProps) {
+ const { url, handleBack } = props;
+ const formHooks = useForm({
+ defaultValues: {
+ flake_url: url,
+ flake_attr: "vm1",
+ cores: 1,
+ graphics: true,
+ memory_size: 1024,
+ },
+ });
+ const [vmUuid, setVmUuid] = useState(null);
+
+ const { setValue, watch, formState, handleSubmit } = formHooks;
+ const { config, error, isLoading } = useVms({
+ url,
+ // TODO: FIXME
+ attr: watch("flake_attr"),
+ });
+ useEffect(() => {
+ if (config) {
+ setValue("cores", config?.cores);
+ setValue("memory_size", config?.memory_size);
+ setValue("graphics", config?.graphics);
+ }
+ }, [config, setValue]);
+
+ return (
+
+ {!formState.isSubmitted && (
+ <>
+ {error && (
+
+ Error
+ An Error occurred - See details below
+
+ )}
+
+ {isLoading && (
+ }
+ />
+ )}
+ {config && (
+
+ )}
+ {error && (
+ <>
+
+ err.msg.split("\n"))
+ ?.flat()
+ .filter(Boolean) || []
+ }
+ />
+ >
+ )}
+
+ >
+ )}
+
+ {formState.isSubmitted && vmUuid &&
}
+
+ );
+}
diff --git a/pkgs/ui/src/components/join/join.tsx b/pkgs/ui/src/components/join/join.tsx
deleted file mode 100644
index 5c1c60ee4..000000000
--- a/pkgs/ui/src/components/join/join.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-"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/layout.tsx b/pkgs/ui/src/components/join/layout.tsx
new file mode 100644
index 000000000..3966109ec
--- /dev/null
+++ b/pkgs/ui/src/components/join/layout.tsx
@@ -0,0 +1,20 @@
+"use client";
+import { Typography } from "@mui/material";
+import { ReactNode } from "react";
+
+interface LayoutProps {
+ children: ReactNode;
+}
+export const Layout = (props: LayoutProps) => {
+ return (
+
+
+ Join{" "}
+
+ Clan.lol
+
+
+ {props.children}
+
+ );
+};
diff --git a/pkgs/ui/src/components/join/loadingOverlay.tsx b/pkgs/ui/src/components/join/loadingOverlay.tsx
index 43db2adff..63c5381eb 100644
--- a/pkgs/ui/src/components/join/loadingOverlay.tsx
+++ b/pkgs/ui/src/components/join/loadingOverlay.tsx
@@ -1,3 +1,4 @@
+"use client";
import { LinearProgress, Typography } from "@mui/material";
interface LoadingOverlayProps {
diff --git a/pkgs/ui/src/components/join/log.tsx b/pkgs/ui/src/components/join/log.tsx
index bb7b2f330..c70de163b 100644
--- a/pkgs/ui/src/components/join/log.tsx
+++ b/pkgs/ui/src/components/join/log.tsx
@@ -1,3 +1,4 @@
+"use client";
interface LogOptions {
lines: string[];
title?: string;
diff --git a/pkgs/ui/src/components/join/vmBuildLogs.tsx b/pkgs/ui/src/components/join/vmBuildLogs.tsx
new file mode 100644
index 000000000..72440bfd4
--- /dev/null
+++ b/pkgs/ui/src/components/join/vmBuildLogs.tsx
@@ -0,0 +1,34 @@
+"use client";
+import { useGetVmLogs } from "@/api/default/default";
+import { Log } from "./log";
+import { LoadingOverlay } from "./loadingOverlay";
+
+interface VmBuildLogsProps {
+ vmUuid: string;
+}
+export const VmBuildLogs = (props: VmBuildLogsProps) => {
+ const { vmUuid } = props;
+
+ const {
+ data: logs,
+ isLoading,
+ error,
+ } = useGetVmLogs(vmUuid as string, {
+ swr: {
+ enabled: vmUuid !== null,
+ },
+ axios: {
+ responseType: "stream",
+ },
+ });
+
+ return (
+
+ {isLoading && }
+
+
+ );
+};
diff --git a/pkgs/ui/src/components/noDataOverlay/index.tsx b/pkgs/ui/src/components/noDataOverlay/index.tsx
index eb8cc6f48..11867cd47 100644
--- a/pkgs/ui/src/components/noDataOverlay/index.tsx
+++ b/pkgs/ui/src/components/noDataOverlay/index.tsx
@@ -1,5 +1,5 @@
-/* eslint-disable tailwindcss/no-custom-classname */
"use client";
+/* eslint-disable tailwindcss/no-custom-classname */
import * as React from "react";
import Box from "@mui/material/Box";