Merge pull request 'AppState context add' (#382) from feat/join-workflow into main

This commit is contained in:
clan-bot
2023-10-03 08:58:13 +00:00
29 changed files with 484 additions and 364 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -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 (
<div
className={tw`col-span-full row-span-${rowSpan || 1} xl:col-span-1 ${sx}`}
>
{children}
</div>
);
};
interface DashboardPanelProps {
children?: React.ReactNode;
}
const DashboardPanel = (props: DashboardPanelProps) => {
const { children } = props;
return (
<div className="col-span-full row-span-1 xl:col-span-2">{children}</div>
);
};
export default function Dashboard() {
return (
<div className="flex h-screen w-full">
<div className="grid w-full auto-rows-max grid-cols-1 grid-rows-none gap-4 xl:grid-cols-2 2xl:grid-cols-3 ">
<DashboardCard rowSpan={2}>
<NetworkOverview />
</DashboardCard>
<DashboardCard rowSpan={2}>
<RecentActivity />
</DashboardCard>
<DashboardCard>
<Notifications />
</DashboardCard>
<DashboardCard>
<QuickActions />
</DashboardCard>
<DashboardPanel>
<AppOverview />
</DashboardPanel>
<DashboardCard sx={tw`xl:col-span-full 2xl:col-span-1`}>
<TaskQueue />
</DashboardCard>
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
import JoinPrequel from "@/views/joinPrequel";
export default function Page() {
return <JoinPrequel />;
}

View File

@@ -4,13 +4,14 @@ import "./globals.css";
import localFont from "next/font/local"; import localFont from "next/font/local";
import * as React from "react"; import * as React from "react";
import { import {
Button,
CssBaseline, CssBaseline,
IconButton, IconButton,
ThemeProvider, ThemeProvider,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { ChangeEvent, useState } from "react"; import { useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { StyledEngineProvider } from "@mui/material/styles"; import { StyledEngineProvider } from "@mui/material/styles";
@@ -20,7 +21,14 @@ import MenuIcon from "@mui/icons-material/Menu";
import Image from "next/image"; import Image from "next/image";
import { tw } from "@/utils/tailwind"; import { tw } from "@/utils/tailwind";
import axios from "axios"; 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({ const roboto = localFont({
src: [ src: [
@@ -37,6 +45,17 @@ axios.defaults.baseURL = "http://localhost:2979";
// add negative margin for smooth transition to fill the space of the sidebar // add negative margin for smooth transition to fill the space of the sidebar
const translate = tw`lg:-ml-64 -ml-14`; 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({ export default function RootLayout({
children, children,
}: { }: {
@@ -65,9 +84,9 @@ export default function RootLayout({
} }
}, [userPrefersDarkmode, useDarkTheme, setUseDarkTheme]); }, [userPrefersDarkmode, useDarkTheme, setUseDarkTheme]);
const changeThemeHandler = (target: ChangeEvent, currentValue: boolean) => { // const changeThemeHandler = (target: ChangeEvent, currentValue: boolean) => {
setUseDarkTheme(currentValue); // setUseDarkTheme(currentValue);
}; // };
return ( return (
<html lang="en"> <html lang="en">
@@ -82,15 +101,25 @@ export default function RootLayout({
<body id="__next" className={roboto.className}> <body id="__next" className={roboto.className}>
<CssBaseline /> <CssBaseline />
<Toaster /> <Toaster />
<MachineContextProvider> <WithAppState>
<AppContext.Consumer>
{(appState) => {
const showSidebarDerived = Boolean(
showSidebar &&
!appState.isLoading &&
appState.data.isJoined,
);
return (
<>
<Background />
<div className="flex h-screen overflow-hidden"> <div className="flex h-screen overflow-hidden">
<Sidebar <Sidebar
show={showSidebar} show={showSidebarDerived}
onClose={() => setShowSidebar(false)} onClose={() => setShowSidebar(false)}
/> />
<div <div
className={tw`${ className={tw`${
!showSidebar && translate !showSidebarDerived && translate
} flex h-full w-full flex-col overflow-y-scroll transition-[margin] duration-150 ease-in-out`} } flex h-full w-full flex-col overflow-y-scroll transition-[margin] duration-150 ease-in-out`}
> >
<div className="static top-0 mb-2 py-2"> <div className="static top-0 mb-2 py-2">
@@ -100,10 +129,12 @@ export default function RootLayout({
hidden={true} hidden={true}
onClick={() => setShowSidebar((c) => !c)} onClick={() => setShowSidebar((c) => !c)}
> >
{!showSidebar && <MenuIcon />} {!showSidebar && appState.data.isJoined && (
<MenuIcon />
)}
</IconButton> </IconButton>
</div> </div>
<div className="col-span-1 block w-full text-center font-semibold text-white lg:hidden "> <div className="col-span-1 block w-full bg-fixed text-center font-semibold text-white lg:hidden">
<Image <Image
src="/logo.svg" src="/logo.svg"
alt="Clan Logo" alt="Clan Logo"
@@ -117,12 +148,31 @@ export default function RootLayout({
<div className="px-1"> <div className="px-1">
<div className="relative flex h-full flex-1 flex-col"> <div className="relative flex h-full flex-1 flex-col">
<main>{children}</main> <main>
{/* <AutoRedirectEffect /> */}
<Button
fullWidth
onClick={() => {
appState.setAppState((s) => ({
...s,
isJoined: !s.isJoined,
}));
}}
>
Toggle Joined
</Button>
{children}
</main>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</MachineContextProvider> </>
);
}}
</AppContext.Consumer>
</WithAppState>
</body> </body>
</ThemeProvider> </ThemeProvider>
</StyledEngineProvider> </StyledEngineProvider>

View File

@@ -1,79 +1,86 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import { RecentActivity } from "@/components/dashboard/activity";
import { import { AppOverview } from "@/components/dashboard/appOverview";
Button, import { NetworkOverview } from "@/components/dashboard/NetworkOverview";
IconButton, import { Notifications } from "@/components/dashboard/notifications";
Input, import { QuickActions } from "@/components/dashboard/quickActions";
InputAdornment, import { TaskQueue } from "@/components/dashboard/taskQueue";
Paper, import { useAppState } from "@/components/hooks/useAppContext";
TextField, import { MachineContextProvider } from "@/components/hooks/useMachines";
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 { LoadingOverlay } from "@/components/join/loadingOverlay";
import { FlakeBadge } from "@/components/flakeBadge/flakeBadge"; import { tw } from "@/utils/tailwind";
import { Log } from "@/components/join/log"; import JoinPrequel from "@/views/joinPrequel";
import { useForm, SubmitHandler, Controller } from "react-hook-form"; interface DashboardCardProps {
import { Confirm } from "@/components/join/confirm"; children?: React.ReactNode;
import { Layout } from "@/components/join/layout"; rowSpan?: number;
import { ChevronRight } from "@mui/icons-material"; sx?: string;
}
type FormValues = { const DashboardCard = (props: DashboardCardProps) => {
flakeUrl: string; const { children, rowSpan, sx = "" } = props;
flakeAttribute: string; return (
<div
className={tw`col-span-full row-span-${rowSpan || 1} xl:col-span-1 ${sx}`}
>
{children}
</div>
);
}; };
export default function Page() { interface DashboardPanelProps {
const queryParams = useSearchParams(); children?: React.ReactNode;
const flakeUrl = queryParams.get("flake") || ""; }
const flakeAttribute = queryParams.get("attr") || "default"; const DashboardPanel = (props: DashboardPanelProps) => {
const { handleSubmit, control, formState, getValues, reset } = const { children } = props;
useForm<FormValues>({ defaultValues: { flakeUrl: "" } });
const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data);
return ( return (
<Layout> <div className="col-span-full row-span-1 xl:col-span-2">{children}</div>
{!formState.isSubmitted && !flakeUrl && ( );
<form };
onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-2xl justify-self-center" export default function Dashboard() {
> const { data, isLoading } = useAppState();
<Controller if (isLoading) {
name="flakeUrl" return (
control={control} <div className="grid h-full place-items-center">
render={({ field }) => ( <div className="mt-8 w-full max-w-xl">
<Input <LoadingOverlay
{...field} title="Clan Experience"
// variant="standard" subtitle="Loading"
// label="Clan url" variant="circle"
required
fullWidth
startAdornment={
<InputAdornment position="start">Clan Url:</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<IconButton type="submit">
<ChevronRight />
</IconButton>
</InputAdornment>
}
// }}
/> />
)} </div>
/> </div>
</form>
)}
{(formState.isSubmitted || flakeUrl) && (
<Confirm
handleBack={() => reset()}
flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl}
/>
)}
</Layout>
); );
} }
if (!data.isJoined) {
return <JoinPrequel />;
}
if (data.isJoined) {
return (
<MachineContextProvider>
<div className="flex h-screen w-full">
<div className="grid w-full auto-rows-max grid-cols-1 grid-rows-none gap-4 xl:grid-cols-2 2xl:grid-cols-3 ">
<DashboardCard rowSpan={2}>
<NetworkOverview />
</DashboardCard>
<DashboardCard rowSpan={2}>
<RecentActivity />
</DashboardCard>
<DashboardCard>
<Notifications />
</DashboardCard>
<DashboardCard>
<QuickActions />
</DashboardCard>
<DashboardPanel>
<AppOverview />
</DashboardPanel>
<DashboardCard sx={tw`xl:col-span-full 2xl:col-span-1`}>
<TaskQueue />
</DashboardCard>
</div>
</div>
</MachineContextProvider>
);
}
}

View File

@@ -7,16 +7,12 @@ import {
Edit, Edit,
Group, Group,
Key, Key,
MenuOpen,
NetworkCell,
Settings, Settings,
SettingsEthernet, SettingsEthernet,
VisibilityOff,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { import {
Avatar, Avatar,
Button, Button,
Divider,
IconButton, IconButton,
List, List,
ListItem, ListItem,
@@ -29,14 +25,13 @@ import {
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { useListMachines } from "@/api/default/default"; // import { useListMachines } from "@/api/default/default";
export async function generateStaticParams() { export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }]; return [{ id: "1" }, { id: "2" }];
} }
function getTemplate(params: { id: string }) { function getTemplate(params: { id: string }) {
console.log({ params });
// const res = await fetch(`https://.../posts/${params.id}`); // const res = await fetch(`https://.../posts/${params.id}`);
return { return {
short: `My Template ${params.id}`, short: `My Template ${params.id}`,
@@ -47,8 +42,7 @@ interface TemplateDetailProps {
params: { id: string }; params: { id: string };
} }
export default function TemplateDetail({ params }: TemplateDetailProps) { export default function TemplateDetail({ params }: TemplateDetailProps) {
const { data, isLoading } = useListMachines(); // const { data, isLoading } = useListMachines();
console.log({ data, isLoading });
const details = getTemplate(params); const details = getTemplate(params);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

View File

@@ -2,7 +2,6 @@ import { ChevronRight } from "@mui/icons-material";
import { import {
Avatar, Avatar,
Divider, Divider,
IconButton,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,

View File

@@ -0,0 +1,45 @@
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 (
<div
className={
"fixed -z-10 h-[100vh] w-[100vw] overflow-hidden opacity-10 blur-md dark:opacity-40"
}
>
{(isLoading || !data.isJoined) && (
<>
<Image
className="dark:hidden"
alt="clan"
src={clanLight}
placeholder="blur"
quality={100}
fill
sizes="100vw"
style={{
objectFit: "cover",
}}
/>
<Image
className="hidden dark:block"
alt="clan"
src={clanDark}
placeholder="blur"
quality={100}
fill
sizes="100vw"
style={{
objectFit: "cover",
}}
/>
</>
)}
</div>
);
}

View File

@@ -12,7 +12,7 @@ import {
Paper, Paper,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { IChangeEvent, FormProps } from "@rjsf/core"; import { IChangeEvent } from "@rjsf/core";
import { Form } from "@rjsf/mui"; import { Form } from "@rjsf/mui";
import validator from "@rjsf/validator-ajv8"; import validator from "@rjsf/validator-ajv8";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -100,7 +100,7 @@ function ErrorList<
} }
function PureCustomConfig(props: PureCustomConfigProps) { function PureCustomConfig(props: PureCustomConfigProps) {
const { schema, initialValues, formHooks } = props; const { schema, formHooks } = props;
const { setValue, watch } = formHooks; const { setValue, watch } = formHooks;
console.log({ schema }); console.log({ schema });

View File

@@ -5,19 +5,14 @@ import {
Step, Step,
StepLabel, StepLabel,
Stepper, Stepper,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import React, { ReactNode, useState } from "react"; import React, { useState } from "react";
import { useForm, UseFormReturn } from "react-hook-form"; import { useForm } from "react-hook-form";
import { CustomConfig } from "./customConfig"; import { CustomConfig } from "./customConfig";
import { CreateMachineForm, FormStep } from "./interfaces"; import { CreateMachineForm, FormStep } from "./interfaces";
const SC = (props: { children: ReactNode }) => {
return <>{props.children}</>;
};
export function CreateMachineForm() { export function CreateMachineForm() {
const formHooks = useForm<CreateMachineForm>({ const formHooks = useForm<CreateMachineForm>({
defaultValues: { defaultValues: {
@@ -25,7 +20,7 @@ export function CreateMachineForm() {
config: {}, config: {},
}, },
}); });
const { handleSubmit, control, watch, reset, formState } = formHooks; const { handleSubmit, reset } = formHooks;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [activeStep, setActiveStep] = useState<number>(0); const [activeStep, setActiveStep] = useState<number>(0);

View File

@@ -1,4 +1,4 @@
import { ReactElement, ReactNode } from "react"; import { ReactElement } from "react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
export type StepId = "template" | "modules" | "config" | "save"; export type StepId = "template" | "modules" | "config" | "save";

View File

@@ -8,7 +8,6 @@ import {
ListItem, ListItem,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
Typography,
} from "@mui/material"; } from "@mui/material";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";

View File

@@ -1,7 +1,5 @@
import { DashboardCard } from "@/components/card"; import { DashboardCard } from "@/components/card";
import Image from "next/image"; import Image from "next/image";
import { ReactNode } from "react";
interface AppCardProps { interface AppCardProps {
name: string; name: string;
icon?: string; icon?: string;
@@ -32,11 +30,6 @@ const AppCard = (props: AppCardProps) => {
); );
}; };
type App = {
name: string;
icon?: string;
};
const apps = [ const apps = [
{ {
name: "Firefox", name: "Firefox",

View File

@@ -1,17 +1,12 @@
import { DashboardCard } from "@/components/card"; import { DashboardCard } from "@/components/card";
import { notificationData } from "@/data/dashboardData"; import { notificationData } from "@/data/dashboardData";
import { tw } from "@/utils/tailwind";
import { import {
Avatar, Avatar,
Chip,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText, ListItemText,
} from "@mui/material"; } from "@mui/material";
import { Label } from "recharts";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { DashboardCard } from "@/components/card"; import { DashboardCard } from "@/components/card";
import { Button, Fab } from "@mui/material"; import { Fab } from "@mui/material";
import { MouseEventHandler, ReactNode } from "react"; import { MouseEventHandler, ReactNode } from "react";
import LanIcon from "@mui/icons-material/Lan"; import LanIcon from "@mui/icons-material/Lan";

View File

@@ -19,7 +19,7 @@ interface TaskEntryProps {
details?: string; details?: string;
} }
const TaskEntry = (props: TaskEntryProps) => { const TaskEntry = (props: TaskEntryProps) => {
const { result, task, details, status } = props; const { result, task, status } = props;
return ( return (
<> <>
<div className="col-span-1">{status}</div> <div className="col-span-1">{status}</div>

View File

@@ -10,6 +10,9 @@ export const FlakeBadge = (props: FlakeBadgeProps) => (
label={`${props.flakeUrl}#${props.flakeAttr}`} label={`${props.flakeUrl}#${props.flakeAttr}`}
sx={{ sx={{
p: 2, p: 2,
"&.MuiChip-root": {
maxWidth: "unset",
},
"&.MuiChip-label": { "&.MuiChip-label": {
overflow: "unset", overflow: "unset",
}, },

View File

@@ -0,0 +1,61 @@
import { useListMachines } from "@/api/default/default";
import { 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<any> | undefined;
setAppState: Dispatch<SetStateAction<AppState>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
};
// const initialState = {
// isLoading: true,
// } as const;
export const AppContext = createContext<AppContextType>({} as AppContextType);
type AppState = {
isJoined?: boolean;
clanName?: string;
};
interface AppContextProviderProps {
children: ReactNode;
}
export const WithAppState = (props: AppContextProviderProps) => {
const { children } = props;
const { isLoading, error, mutate, swrKey } = useListMachines();
const [data, setAppState] = useState<AppState>({ isJoined: false });
return (
<AppContext.Provider
value={{
data,
setAppState,
isLoading,
error,
swrKey,
mutate,
}}
>
{children}
</AppContext.Provider>
);
};
export const useAppState = () => React.useContext(AppContext);

View File

@@ -33,7 +33,9 @@ export const useVms = (options: UseVmsOptions) => {
} catch (e) { } catch (e) {
const err = e as AxiosError<HTTPValidationError>; const err = e as AxiosError<HTTPValidationError>;
setError(err); setError(err);
toast.error(err.message); toast(
"Could not find default configuration. Please select a machine preset",
);
return undefined; return undefined;
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@@ -10,10 +10,11 @@ import {
} from "@mui/material"; } from "@mui/material";
import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form"; import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form";
import { FlakeBadge } from "../flakeBadge/flakeBadge"; import { FlakeBadge } from "../flakeBadge/flakeBadge";
import { createVm, useGetVmLogs } from "@/api/default/default"; import { createVm, useInspectFlakeAttrs } from "@/api/default/default";
import { VmConfig } from "@/api/model"; 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 { toast } from "react-hot-toast";
import { useAppState } from "../hooks/useAppContext";
interface VmPropLabelProps { interface VmPropLabelProps {
children: React.ReactNode; children: React.ReactNode;
@@ -28,20 +29,26 @@ interface VmPropContentProps {
children: React.ReactNode; children: React.ReactNode;
} }
const VmPropContent = (props: VmPropContentProps) => ( const VmPropContent = (props: VmPropContentProps) => (
<div className="col-span-4 font-bold sm:col-span-3">{props.children}</div> <div className="col-span-4 sm:col-span-3">{props.children}</div>
); );
interface VmDetailsProps { interface VmDetailsProps {
vmConfig: VmConfig;
formHooks: UseFormReturn<VmConfig, any, undefined>; formHooks: UseFormReturn<VmConfig, any, undefined>;
setVmUuid: Dispatch<SetStateAction<string | null>>; setVmUuid: Dispatch<SetStateAction<string | null>>;
} }
export const ConfigureVM = (props: VmDetailsProps) => { export const ConfigureVM = (props: VmDetailsProps) => {
const { vmConfig, formHooks, setVmUuid } = props; const { formHooks, setVmUuid } = props;
const { control, handleSubmit } = formHooks; const { control, handleSubmit, watch, setValue } = formHooks;
const { cores, flake_attr, flake_url, graphics, memory_size } = vmConfig;
const [isStarting, setStarting] = useState(false); 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<VmConfig> = async (data) => { const onSubmit: SubmitHandler<VmConfig> = async (data) => {
setStarting(true); setStarting(true);
@@ -53,6 +60,7 @@ export const ConfigureVM = (props: VmDetailsProps) => {
setStarting(false); setStarting(false);
if (response.statusText === "OK") { if (response.statusText === "OK") {
toast.success(("Joined @ " + uuid) as string); toast.success(("Joined @ " + uuid) as string);
setAppState((s) => ({ ...s, isJoined: true }));
} else { } else {
toast.error("Could not join"); toast.error("Could not join");
} }
@@ -64,20 +72,33 @@ export const ConfigureVM = (props: VmDetailsProps) => {
className="grid grid-cols-4 gap-y-10" className="grid grid-cols-4 gap-y-10"
> >
<div className="col-span-4"> <div className="col-span-4">
<ListSubheader>General</ListSubheader> <ListSubheader sx={{ bgcolor: "inherit" }}>General</ListSubheader>
</div> </div>
<VmPropLabel>Flake</VmPropLabel> <VmPropLabel>Flake</VmPropLabel>
<VmPropContent> <VmPropContent>
<FlakeBadge flakeAttr={flake_attr} flakeUrl={flake_url} /> <FlakeBadge
flakeAttr={watch("flake_attr")}
flakeUrl={watch("flake_url")}
/>
</VmPropContent> </VmPropContent>
<VmPropLabel>Machine</VmPropLabel> <VmPropLabel>Machine</VmPropLabel>
<VmPropContent> <VmPropContent>
{!isLoading && (
<Controller <Controller
name="flake_attr" name="flake_attr"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Select {...field} variant="standard" fullWidth> <Select
{["default", "vm1"].map((attr) => ( {...field}
required
variant="standard"
fullWidth
disabled={isLoading}
>
{!data?.data.flake_attrs.includes("default") && (
<MenuItem value={"default"}>default</MenuItem>
)}
{data?.data.flake_attrs.map((attr) => (
<MenuItem value={attr} key={attr}> <MenuItem value={attr} key={attr}>
{attr} {attr}
</MenuItem> </MenuItem>
@@ -85,9 +106,10 @@ export const ConfigureVM = (props: VmDetailsProps) => {
</Select> </Select>
)} )}
/> />
)}
</VmPropContent> </VmPropContent>
<div className="col-span-4"> <div className="col-span-4">
<ListSubheader>VM</ListSubheader> <ListSubheader sx={{ bgcolor: "inherit" }}>VM</ListSubheader>
</div> </div>
<VmPropLabel>CPU Cores</VmPropLabel> <VmPropLabel>CPU Cores</VmPropLabel>
<VmPropContent> <VmPropContent>
@@ -103,7 +125,7 @@ export const ConfigureVM = (props: VmDetailsProps) => {
name="graphics" name="graphics"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Switch {...field} defaultChecked={vmConfig.graphics} /> <Switch {...field} defaultChecked={watch("graphics")} />
)} )}
/> />
</VmPropContent> </VmPropContent>
@@ -129,7 +151,12 @@ export const ConfigureVM = (props: VmDetailsProps) => {
<div className="col-span-4 grid items-center"> <div className="col-span-4 grid items-center">
{isStarting && <LinearProgress />} {isStarting && <LinearProgress />}
<Button type="submit" disabled={isStarting} variant="contained"> <Button
autoFocus
type="submit"
disabled={isStarting}
variant="contained"
>
Join Clan Join Clan
</Button> </Button>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { LoadingOverlay } from "./loadingOverlay"; import { LoadingOverlay } from "./loadingOverlay";
import { FlakeBadge } from "../flakeBadge/flakeBadge"; import { FlakeBadge } from "../flakeBadge/flakeBadge";
import { Typography, Button } from "@mui/material"; import { Typography, Button } from "@mui/material";
import { FlakeResponse } from "@/api/model"; // import { FlakeResponse } from "@/api/model";
import { ConfirmVM } from "./confirmVM"; import { ConfirmVM } from "./confirmVM";
import { Log } from "./log"; import { Log } from "./log";
import GppMaybeIcon from "@mui/icons-material/GppMaybe"; import GppMaybeIcon from "@mui/icons-material/GppMaybe";
@@ -11,22 +11,29 @@ import { useInspectFlake } from "@/api/default/default";
interface ConfirmProps { interface ConfirmProps {
flakeUrl: string; flakeUrl: string;
flakeAttr: string;
handleBack: () => void; handleBack: () => void;
} }
export const Confirm = (props: ConfirmProps) => { export const Confirm = (props: ConfirmProps) => {
const { flakeUrl, handleBack } = props; const { flakeUrl, handleBack, flakeAttr } = props;
const [userConfirmed, setUserConfirmed] = useState(false); const [userConfirmed, setUserConfirmed] = useState(false);
const { data, error, isLoading } = useInspectFlake({ url: flakeUrl }); const { data, isLoading } = useInspectFlake({
url: flakeUrl,
});
return userConfirmed ? ( return userConfirmed ? (
<ConfirmVM url={flakeUrl} handleBack={handleBack} /> <ConfirmVM
url={flakeUrl}
handleBack={handleBack}
defaultFlakeAttr={flakeAttr}
/>
) : ( ) : (
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2 "> <div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2 ">
{isLoading && ( {isLoading && (
<LoadingOverlay <LoadingOverlay
title={"Loading Flake"} title={"Loading Flake"}
subtitle={<FlakeBadge flakeUrl={flakeUrl} flakeAttr="" />} subtitle={<FlakeBadge flakeUrl={flakeUrl} flakeAttr={flakeAttr} />}
/> />
)} )}
{data && ( {data && (

View File

@@ -3,43 +3,36 @@ import React, { useEffect, useState } from "react";
import { VmConfig } from "@/api/model"; import { VmConfig } from "@/api/model";
import { useVms } from "@/components/hooks/useVms"; 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 { LoadingOverlay } from "./loadingOverlay";
import { FlakeBadge } from "../flakeBadge/flakeBadge"; import { useForm } from "react-hook-form";
import { Log } from "./log";
import { SubmitHandler, useForm } from "react-hook-form";
import { ConfigureVM } from "./configureVM"; import { ConfigureVM } from "./configureVM";
import { VmBuildLogs } from "./vmBuildLogs"; import { VmBuildLogs } from "./vmBuildLogs";
interface ConfirmVMProps { interface ConfirmVMProps {
url: string; url: string;
handleBack: () => void; handleBack: () => void;
defaultFlakeAttr: string;
} }
export function ConfirmVM(props: ConfirmVMProps) { export function ConfirmVM(props: ConfirmVMProps) {
const { url, handleBack } = props; const { url, defaultFlakeAttr } = props;
const formHooks = useForm<VmConfig>({ const formHooks = useForm<VmConfig>({
defaultValues: { defaultValues: {
flake_url: url, flake_url: url,
flake_attr: "vm1", flake_attr: defaultFlakeAttr,
cores: 1, cores: 4,
graphics: true, graphics: true,
memory_size: 1024, memory_size: 2048,
}, },
}); });
const [vmUuid, setVmUuid] = useState<string | null>(null); const [vmUuid, setVmUuid] = useState<string | null>(null);
const { setValue, watch, formState, handleSubmit } = formHooks; const { setValue, watch, formState } = formHooks;
const { config, error, isLoading } = useVms({ const { config, isLoading } = useVms({
url, url,
// TODO: FIXME attr: watch("flake_attr") || defaultFlakeAttr,
attr: watch("flake_attr"),
}); });
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setValue("cores", config?.cores); setValue("cores", config?.cores);
@@ -52,48 +45,12 @@ export function ConfirmVM(props: ConfirmVMProps) {
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2"> <div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2">
{!formState.isSubmitted && ( {!formState.isSubmitted && (
<> <>
{error && (
<Alert severity="error" className="w-full max-w-2xl">
<AlertTitle>Error</AlertTitle>
An Error occurred - See details below
</Alert>
)}
<div className="mb-2 w-full max-w-2xl"> <div className="mb-2 w-full max-w-2xl">
{isLoading && ( {isLoading && (
<LoadingOverlay <LoadingOverlay title={"Loading VM Configuration"} subtitle="" />
title={"Loading VM Configuration"}
subtitle={<FlakeBadge flakeUrl={url} flakeAttr={url} />}
/>
)}
{config && (
<ConfigureVM
vmConfig={config}
formHooks={formHooks}
setVmUuid={setVmUuid}
/>
)}
{error && (
<>
<Button
color="error"
fullWidth
variant="contained"
onClick={handleBack}
className="my-2"
>
Back
</Button>
<Log
title="Log"
lines={
error?.response?.data?.detail
?.map((err, idx) => err.msg.split("\n"))
?.flat()
.filter(Boolean) || []
}
/>
</>
)} )}
<ConfigureVM formHooks={formHooks} setVmUuid={setVmUuid} />
</div> </div>
</> </>
)} )}

View File

@@ -1,18 +1,27 @@
"use client"; "use client";
import { LinearProgress, Typography } from "@mui/material"; import { CircularProgress, LinearProgress, Typography } from "@mui/material";
interface LoadingOverlayProps { interface LoadingOverlayProps {
title: React.ReactNode; title: React.ReactNode;
subtitle: React.ReactNode; subtitle: React.ReactNode;
variant?: "linear" | "circle";
} }
export const LoadingOverlay = (props: LoadingOverlayProps) => { export const LoadingOverlay = (props: LoadingOverlayProps) => {
const { title, subtitle } = props; const { title, subtitle, variant = "linear" } = props;
return ( return (
<div className="w-full"> <div className="w-full">
<Typography variant="subtitle2">{title}</Typography> <div className="grid w-full place-items-center">
<LinearProgress className="mb-2 w-full" /> <Typography variant="subtitle1">{title}</Typography>
<div className="grid w-full place-items-center">{subtitle}</div> </div>
<Typography variant="subtitle1"></Typography> <div className="grid w-full place-items-center">
<Typography variant="subtitle2">{subtitle}</Typography>
</div>
{variant === "linear" && <LinearProgress className="my-2 w-full" />}
{variant === "circle" && (
<div className="grid w-full place-items-center">
<CircularProgress className="my-2 w-full" />
</div>
)}
</div> </div>
); );
}; };

View File

@@ -9,11 +9,7 @@ interface VmBuildLogsProps {
export const VmBuildLogs = (props: VmBuildLogsProps) => { export const VmBuildLogs = (props: VmBuildLogsProps) => {
const { vmUuid } = props; const { vmUuid } = props;
const { const { data: logs, isLoading } = useGetVmLogs(vmUuid as string, {
data: logs,
isLoading,
error,
} = useGetVmLogs(vmUuid as string, {
swr: { swr: {
enabled: vmUuid !== null, enabled: vmUuid !== null,
}, },

View File

@@ -1,17 +1,14 @@
import { import {
Alert,
Divider, Divider,
Icon,
IconButton, IconButton,
List, List,
ListItem, ListItem,
ListItemButton, ListItemButton,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
Snackbar,
} from "@mui/material"; } from "@mui/material";
import Image from "next/image"; import Image from "next/image";
import { ReactNode, useState } from "react"; import { ReactNode } from "react";
import DashboardIcon from "@mui/icons-material/Dashboard"; import DashboardIcon from "@mui/icons-material/Dashboard";
import DevicesIcon from "@mui/icons-material/Devices"; import DevicesIcon from "@mui/icons-material/Devices";
@@ -29,7 +26,7 @@ type MenuEntry = {
icon: ReactNode; icon: ReactNode;
label: string; label: string;
to: string; to: string;
missing: boolean; disabled: boolean;
} & { } & {
subMenuEntries?: MenuEntry[]; subMenuEntries?: MenuEntry[];
}; };
@@ -39,37 +36,37 @@ const menuEntries: MenuEntry[] = [
icon: <DashboardIcon />, icon: <DashboardIcon />,
label: "Dashoard", label: "Dashoard",
to: "/", to: "/",
missing: false, disabled: false,
}, },
{ {
icon: <DevicesIcon />, icon: <DevicesIcon />,
label: "Machines", label: "Machines",
to: "/machines", to: "/machines",
missing: false, disabled: false,
}, },
{ {
icon: <AppsIcon />, icon: <AppsIcon />,
label: "Applications", label: "Applications",
to: "/applications", to: "/applications",
missing: true, disabled: true,
}, },
{ {
icon: <LanIcon />, icon: <LanIcon />,
label: "Network", label: "Network",
to: "/network", to: "/network",
missing: true, disabled: true,
}, },
{ {
icon: <DesignServicesIcon />, icon: <DesignServicesIcon />,
label: "Templates", label: "Templates",
to: "/templates", to: "/templates",
missing: false, disabled: false,
}, },
{ {
icon: <BackupIcon />, icon: <BackupIcon />,
label: "Backups", label: "Backups",
to: "/backups", to: "/backups",
missing: true, disabled: true,
}, },
]; ];
@@ -83,22 +80,6 @@ interface SidebarProps {
export function Sidebar(props: SidebarProps) { export function Sidebar(props: SidebarProps) {
const { show, onClose } = props; const { show, onClose } = props;
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(true);
};
const handleClose = (
event?: React.SyntheticEvent | Event,
reason?: string,
) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
return ( return (
<aside <aside
className={tw`${ className={tw`${
@@ -137,10 +118,8 @@ export function Sidebar(props: SidebarProps) {
<ListItemButton <ListItemButton
className="justify-center lg:justify-normal" className="justify-center lg:justify-normal"
LinkComponent={Link} LinkComponent={Link}
href={menuEntry.missing ? "" : menuEntry.to} href={menuEntry.to}
onClickCapture={ disabled={menuEntry.disabled}
menuEntry.missing ? () => handleClick() : undefined
}
> >
<ListItemIcon <ListItemIcon
color="inherit" color="inherit"
@@ -160,12 +139,6 @@ export function Sidebar(props: SidebarProps) {
); );
})} })}
</List> </List>
<Snackbar open={open} autoHideDuration={2000} onClose={handleClose}>
<Alert onClose={handleClose} severity="error" sx={{ width: "100%" }}>
Site does not exist yet
</Alert>
</Snackbar>
<Divider flexItem className="mx-8 my-10 hidden bg-zinc-600 lg:block" /> <Divider flexItem className="mx-8 my-10 hidden bg-zinc-600 lg:block" />
<div className="mx-auto mb-8 hidden w-full max-w-xs rounded-sm px-4 py-6 text-center align-bottom shadow-sm lg:block"> <div className="mx-auto mb-8 hidden w-full max-w-xs rounded-sm px-4 py-6 text-center align-bottom shadow-sm lg:block">
<h3 className="mb-2 w-full font-semibold text-white"> <h3 className="mb-2 w-full font-semibold text-white">

View File

@@ -58,20 +58,20 @@ function getRandomName(): string {
} }
// A function to generate random IPv6 addresses // A function to generate random IPv6 addresses
function getRandomId(): string { // function getRandomId(): string {
let hex = "0123456789abcdef"; // let hex = "0123456789abcdef";
let id = ""; // let id = "";
for (let i = 0; i < 8; i++) { // for (let i = 0; i < 8; i++) {
for (let j = 0; j < 4; j++) { // for (let j = 0; j < 4; j++) {
let index = Math.floor(Math.random() * hex.length); // let index = Math.floor(Math.random() * hex.length);
id += hex[index]; // id += hex[index];
} // }
if (i < 7) { // if (i < 7) {
id += ":"; // id += ":";
} // }
} // }
return id; // return id;
} // }
// A function to generate random status keys // A function to generate random status keys
function getRandomStatus(): NodeStatusKeys { function getRandomStatus(): NodeStatusKeys {

View File

@@ -0,0 +1,63 @@
"use client";
import React from "react";
import { IconButton, Input, InputAdornment } from "@mui/material";
import { useSearchParams } from "next/navigation";
import { useForm, 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 flakeAttr = queryParams.get("attr") || "default";
const { handleSubmit, control, formState, getValues, reset } =
useForm<FormValues>({ defaultValues: { flakeUrl: "" } });
return (
<Layout>
{!formState.isSubmitted && !flakeUrl && (
<form
onSubmit={handleSubmit(() => {})}
className="w-full max-w-2xl justify-self-center"
>
<Controller
name="flakeUrl"
control={control}
render={({ field }) => (
<Input
color="secondary"
aria-required="true"
{...field}
required
fullWidth
startAdornment={
<InputAdornment position="start">Clan</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<IconButton type="submit">
<ChevronRight />
</IconButton>
</InputAdornment>
}
/>
)}
/>
</form>
)}
{(formState.isSubmitted || flakeUrl) && (
<Confirm
handleBack={() => reset()}
flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl}
flakeAttr={flakeAttr}
/>
)}
</Layout>
);
}

View File

@@ -14,6 +14,7 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"noUnusedLocals": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"