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 && ( +
+ ( + Clan Url: + } + endAdornment={ + + + + + + } + // }} + /> + )} + /> + + )} + {(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 ( +
+
+ General +
+ Flake + + + + Machine + + ( + + )} + /> + +
+ VM +
+ CPU Cores + + } + /> + + Graphics + + ( + + )} + /> + + Memory Size + + + ( + MiB + ), + }} + /> + )} + /> + + +
+ {isStarting && } + +
+
+ ); +}; 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";