diff --git a/pkgs/ui/public/clan-dark.png b/pkgs/ui/public/clan-dark.png new file mode 100644 index 000000000..dfeea5e21 Binary files /dev/null and b/pkgs/ui/public/clan-dark.png differ diff --git a/pkgs/ui/public/clan-white.png b/pkgs/ui/public/clan-white.png new file mode 100644 index 000000000..21735dd1a Binary files /dev/null and b/pkgs/ui/public/clan-white.png differ diff --git a/pkgs/ui/src/app/dashboard/page.tsx b/pkgs/ui/src/app/dashboard/page.tsx deleted file mode 100644 index c230c6a2c..000000000 --- a/pkgs/ui/src/app/dashboard/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"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/layout.tsx b/pkgs/ui/src/app/layout.tsx index ec03ba894..53dde9326 100644 --- a/pkgs/ui/src/app/layout.tsx +++ b/pkgs/ui/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import localFont from "next/font/local"; import * as React from "react"; import { + Button, CssBaseline, IconButton, ThemeProvider, @@ -20,7 +21,14 @@ import MenuIcon from "@mui/icons-material/Menu"; import Image from "next/image"; import { tw } from "@/utils/tailwind"; import axios from "axios"; -import { MachineContextProvider } from "@/components/hooks/useMachines"; + +import { + AppContext, + WithAppState, + useAppState, +} from "@/components/hooks/useAppContext"; +import Background from "@/components/background"; +import { usePathname, redirect } from "next/navigation"; const roboto = localFont({ src: [ @@ -37,6 +45,17 @@ axios.defaults.baseURL = "http://localhost:2979"; // add negative margin for smooth transition to fill the space of the sidebar const translate = tw`lg:-ml-64 -ml-14`; +const AutoRedirectEffect = () => { + const { isLoading, data } = useAppState(); + const pathname = usePathname(); + React.useEffect(() => { + if (!isLoading && !data.isJoined && pathname !== "/") { + redirect("/"); + } + }, [isLoading, data, pathname]); + return <>; +}; + export default function RootLayout({ children, }: { @@ -82,47 +101,78 @@ export default function RootLayout({ - -
- setShowSidebar(false)} - /> -
-
-
-
- -
-
- Clan Logo + + {(appState) => { + const showSidebarDerived = Boolean( + showSidebar && + !appState.isLoading && + appState.data.isJoined, + ); + return ( + <> + +
+ setShowSidebar(false)} /> -
-
-
+
+
+
+
+ +
+
+ Clan Logo +
+
+
-
-
-
{children}
-
-
-
-
- +
+
+
+ + + + {children} +
+
+
+
+
+ + ); + }} + + diff --git a/pkgs/ui/src/app/page.tsx b/pkgs/ui/src/app/page.tsx index 40b43ee71..ba757c688 100644 --- a/pkgs/ui/src/app/page.tsx +++ b/pkgs/ui/src/app/page.tsx @@ -1,79 +1,72 @@ "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"; +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 { useAppState } from "@/components/hooks/useAppContext"; +import { MachineContextProvider } from "@/components/hooks/useMachines"; +import { tw } from "@/utils/tailwind"; +import JoinPrequel from "@/views/joinPrequel"; -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 DashboardCardProps { + children?: React.ReactNode; + rowSpan?: number; + sx?: string; +} +const DashboardCard = (props: DashboardCardProps) => { + const { children, rowSpan, sx = "" } = 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); - - return ( - - {!formState.isSubmitted && !flakeUrl && ( -
- ( - Clan Url: - } - endAdornment={ - - - - - - } - // }} - /> - )} - /> - - )} - {(formState.isSubmitted || flakeUrl) && ( - reset()} - flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl} - /> - )} -
- ); +interface DashboardPanelProps { + children?: React.ReactNode; +} +const DashboardPanel = (props: DashboardPanelProps) => { + const { children } = props; + return ( +
{children}
+ ); +}; + +export default function Dashboard() { + const { data } = useAppState(); + if (!data.isJoined) { + return ; + } + if (data.isJoined) { + return ( + +
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ ); + } } diff --git a/pkgs/ui/src/app/templates/[id]/page.tsx b/pkgs/ui/src/app/templates/[id]/page.tsx index bfdbfa036..d6b8ac365 100644 --- a/pkgs/ui/src/app/templates/[id]/page.tsx +++ b/pkgs/ui/src/app/templates/[id]/page.tsx @@ -36,7 +36,6 @@ export async function generateStaticParams() { } function getTemplate(params: { id: string }) { - console.log({ params }); // const res = await fetch(`https://.../posts/${params.id}`); return { short: `My Template ${params.id}`, @@ -48,7 +47,6 @@ interface TemplateDetailProps { } export default function TemplateDetail({ params }: TemplateDetailProps) { const { data, isLoading } = useListMachines(); - console.log({ data, isLoading }); const details = getTemplate(params); const [anchorEl, setAnchorEl] = useState(null); diff --git a/pkgs/ui/src/components/background.tsx b/pkgs/ui/src/components/background.tsx new file mode 100644 index 000000000..288dba2bf --- /dev/null +++ b/pkgs/ui/src/components/background.tsx @@ -0,0 +1,51 @@ +import Image from "next/image"; +import clanLight from "../../public/clan-dark.png"; +import clanDark from "../../public/clan-dark.png"; +import { useAppState } from "./hooks/useAppContext"; + +export default function Background() { + const { data, isLoading } = useAppState(); + + return ( +
+ {(isLoading || !data.isJoined) && ( + <> + clan + clan + + )} +
+ ); +} + +// position: fixed; +// height: 100vh; +// width: 100vw; +// overflow: hidden; +// z-index: -1; diff --git a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx index 1b0b91a72..8871de485 100644 --- a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx +++ b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx @@ -10,7 +10,10 @@ export const FlakeBadge = (props: FlakeBadgeProps) => ( label={`${props.flakeUrl}#${props.flakeAttr}`} sx={{ p: 2, - "& .MuiChip-label": { + "&.MuiChip-root": { + maxWidth: "unset", + }, + "&.MuiChip-label": { overflow: "unset", }, }} diff --git a/pkgs/ui/src/components/hooks/useAppContext.tsx b/pkgs/ui/src/components/hooks/useAppContext.tsx new file mode 100644 index 000000000..883ec128c --- /dev/null +++ b/pkgs/ui/src/components/hooks/useAppContext.tsx @@ -0,0 +1,61 @@ +import { useListMachines } from "@/api/default/default"; +import { Machine, MachinesResponse } from "@/api/model"; +import { AxiosError, AxiosResponse } from "axios"; +import React, { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useState, +} from "react"; +import { KeyedMutator } from "swr"; + +type AppContextType = { + // data: AxiosResponse<{}, any> | undefined; + data: AppState; + + isLoading: boolean; + error: AxiosError | undefined; + + setAppState: Dispatch>; + mutate: KeyedMutator>; + swrKey: string | false | Record; +}; + +const initialState = { + isLoading: true, +} as const; + +export const AppContext = createContext({} as AppContextType); + +type AppState = { + isJoined?: boolean; + clanName?: string; +}; + +interface AppContextProviderProps { + children: ReactNode; +} +export const WithAppState = (props: AppContextProviderProps) => { + const { children } = props; + const { data: rawData, isLoading, error, mutate, swrKey } = useListMachines(); + + const [data, setAppState] = useState({ isJoined: false }); + + return ( + + {children} + + ); +}; + +export const useAppState = () => React.useContext(AppContext); diff --git a/pkgs/ui/src/components/hooks/useVms.tsx b/pkgs/ui/src/components/hooks/useVms.tsx index 212d290ea..bccfe77ad 100644 --- a/pkgs/ui/src/components/hooks/useVms.tsx +++ b/pkgs/ui/src/components/hooks/useVms.tsx @@ -33,7 +33,9 @@ export const useVms = (options: UseVmsOptions) => { } catch (e) { const err = e as AxiosError; setError(err); - toast.error(err.message); + toast( + "Could not find default configuration. Please select a machine preset", + ); return undefined; } finally { setIsLoading(false); diff --git a/pkgs/ui/src/components/join/configureVM.tsx b/pkgs/ui/src/components/join/configureVM.tsx index e1541f619..2ed6e9a6f 100644 --- a/pkgs/ui/src/components/join/configureVM.tsx +++ b/pkgs/ui/src/components/join/configureVM.tsx @@ -10,10 +10,15 @@ import { } from "@mui/material"; import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form"; import { FlakeBadge } from "../flakeBadge/flakeBadge"; -import { createVm, useGetVmLogs } from "@/api/default/default"; +import { + createVm, + useGetVmLogs, + useInspectFlakeAttrs, +} from "@/api/default/default"; import { VmConfig } from "@/api/model"; -import { Dispatch, SetStateAction, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import { useAppState } from "../hooks/useAppContext"; interface VmPropLabelProps { children: React.ReactNode; @@ -28,20 +33,26 @@ interface VmPropContentProps { children: React.ReactNode; } const VmPropContent = (props: VmPropContentProps) => ( -
{props.children}
+
{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 { formHooks, setVmUuid } = props; + const { control, handleSubmit, watch, setValue } = formHooks; const [isStarting, setStarting] = useState(false); + const { setAppState } = useAppState(); + const { isLoading, data } = useInspectFlakeAttrs({ url: watch("flake_url") }); + + useEffect(() => { + if (!isLoading && data?.data) { + setValue("flake_attr", data.data.flake_attrs[0] || ""); + } + }, [isLoading, setValue, data]); const onSubmit: SubmitHandler = async (data) => { setStarting(true); @@ -53,6 +64,7 @@ export const ConfigureVM = (props: VmDetailsProps) => { setStarting(false); if (response.statusText === "OK") { toast.success(("Joined @ " + uuid) as string); + setAppState((s) => ({ ...s, isJoined: true })); } else { toast.error("Could not join"); } @@ -64,30 +76,40 @@ export const ConfigureVM = (props: VmDetailsProps) => { className="grid grid-cols-4 gap-y-10" >
- General + General
Flake - + Machine - ( - - )} - /> + {!isLoading && ( + ( + + )} + /> + )}
- VM + VM
CPU Cores @@ -103,7 +125,7 @@ export const ConfigureVM = (props: VmDetailsProps) => { name="graphics" control={control} render={({ field }) => ( - + )} /> @@ -129,7 +151,12 @@ export const ConfigureVM = (props: VmDetailsProps) => {
{isStarting && } -
diff --git a/pkgs/ui/src/components/join/confirm.tsx b/pkgs/ui/src/components/join/confirm.tsx index cf04bdfe3..43856e86d 100644 --- a/pkgs/ui/src/components/join/confirm.tsx +++ b/pkgs/ui/src/components/join/confirm.tsx @@ -22,7 +22,7 @@ export const Confirm = (props: ConfirmProps) => { return userConfirmed ? ( ) : ( -
+
{isLoading && ( ({ defaultValues: { flake_url: url, - flake_attr: "vm1", - cores: 1, + flake_attr: "default", + cores: 4, graphics: true, - memory_size: 1024, + memory_size: 2048, }, }); const [vmUuid, setVmUuid] = useState(null); @@ -37,7 +37,6 @@ export function ConfirmVM(props: ConfirmVMProps) { const { setValue, watch, formState, handleSubmit } = formHooks; const { config, error, isLoading } = useVms({ url, - // TODO: FIXME attr: watch("flake_attr"), }); useEffect(() => { @@ -52,12 +51,12 @@ export function ConfirmVM(props: ConfirmVMProps) {
{!formState.isSubmitted && ( <> - {error && ( + {/* {error && ( Error An Error occurred - See details below - )} + )} */}
{isLoading && ( } /> )} - {config && ( - - )} - {error && ( + + + + {/* {error && ( <>
)} diff --git a/pkgs/ui/src/views/joinPrequel.tsx b/pkgs/ui/src/views/joinPrequel.tsx new file mode 100644 index 000000000..100a7a719 --- /dev/null +++ b/pkgs/ui/src/views/joinPrequel.tsx @@ -0,0 +1,67 @@ +"use client"; +import React from "react"; +import { + FormControl, + FormHelperText, + IconButton, + Input, + InputAdornment, +} from "@mui/material"; +import { useSearchParams } from "next/navigation"; + +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; +}; + +export default function JoinPrequel() { + const queryParams = useSearchParams(); + const flakeUrl = queryParams.get("flake") || ""; + const { handleSubmit, control, formState, getValues, reset } = + useForm({ defaultValues: { flakeUrl: "" } }); + + return ( + + {!formState.isSubmitted && !flakeUrl && ( +
{})} + className="w-full max-w-2xl justify-self-center" + > + ( + Clan + } + endAdornment={ + + + + + + } + /> + )} + /> + + )} + {(formState.isSubmitted || flakeUrl) && ( + reset()} + flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl} + /> + )} +
+ ); +}