AppState context add
Some checks failed
checks / test (pull_request) Failing after 51s
checks-impure / test (pull_request) Failing after 3h12m51s

This commit is contained in:
Johannes Kirschbauer 2023-10-01 22:47:09 +02:00
parent 82db33d047
commit f9c35ceaa4
Signed by: hsjobeki
GPG Key ID: F62ED8B8BF204685
14 changed files with 406 additions and 220 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

@ -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({
<body id="__next" className={roboto.className}>
<CssBaseline />
<Toaster />
<MachineContextProvider>
<div className="flex h-screen overflow-hidden">
<Sidebar
show={showSidebar}
onClose={() => setShowSidebar(false)}
/>
<div
className={tw`${
!showSidebar && translate
} 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="grid grid-cols-3">
<div className="col-span-1">
<IconButton
hidden={true}
onClick={() => setShowSidebar((c) => !c)}
>
{!showSidebar && <MenuIcon />}
</IconButton>
</div>
<div className="col-span-1 block w-full text-center font-semibold text-white lg:hidden ">
<Image
src="/logo.svg"
alt="Clan Logo"
width={58}
height={58}
priority
<WithAppState>
<AppContext.Consumer>
{(appState) => {
const showSidebarDerived = Boolean(
showSidebar &&
!appState.isLoading &&
appState.data.isJoined,
);
return (
<>
<Background />
<div className="flex h-screen overflow-hidden">
<Sidebar
show={showSidebarDerived}
onClose={() => setShowSidebar(false)}
/>
</div>
</div>
</div>
<div
className={tw`${
!showSidebarDerived && translate
} 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="grid grid-cols-3">
<div className="col-span-1">
<IconButton
hidden={true}
onClick={() => setShowSidebar((c) => !c)}
>
{!showSidebar && appState.data.isJoined && (
<MenuIcon />
)}
</IconButton>
</div>
<div className="col-span-1 block w-full bg-fixed text-center font-semibold text-white lg:hidden">
<Image
src="/logo.svg"
alt="Clan Logo"
width={58}
height={58}
priority
/>
</div>
</div>
</div>
<div className="px-1">
<div className="relative flex h-full flex-1 flex-col">
<main>{children}</main>
</div>
</div>
</div>
</div>
</MachineContextProvider>
<div className="px-1">
<div className="relative flex h-full flex-1 flex-col">
<main>
<AutoRedirectEffect />
<Button
fullWidth
onClick={() => {
appState.setAppState((s) => ({
...s,
isJoined: !s.isJoined,
}));
}}
>
Toggle Joined
</Button>
{children}
</main>
</div>
</div>
</div>
</div>
</>
);
}}
</AppContext.Consumer>
</WithAppState>
</body>
</ThemeProvider>
</StyledEngineProvider>

View File

@ -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 (
<div
className={tw`col-span-full row-span-${rowSpan || 1} xl:col-span-1 ${sx}`}
>
{children}
</div>
);
};
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<FormValues>({ defaultValues: { flakeUrl: "" } });
const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data);
return (
<Layout>
{!formState.isSubmitted && !flakeUrl && (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-2xl justify-self-center"
>
<Controller
name="flakeUrl"
control={control}
render={({ field }) => (
<Input
{...field}
// variant="standard"
// label="Clan url"
required
fullWidth
startAdornment={
<InputAdornment position="start">Clan Url:</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<IconButton type="submit">
<ChevronRight />
</IconButton>
</InputAdornment>
}
// }}
/>
)}
/>
</form>
)}
{(formState.isSubmitted || flakeUrl) && (
<Confirm
handleBack={() => reset()}
flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl}
/>
)}
</Layout>
);
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() {
const { data } = useAppState();
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

@ -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 | HTMLElement>(null);

View File

@ -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 (
<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>
);
}
// position: fixed;
// height: 100vh;
// width: 100vw;
// overflow: hidden;
// z-index: -1;

View File

@ -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",
},
}}

View File

@ -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<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 { data: rawData, 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) {
const err = e as AxiosError<HTTPValidationError>;
setError(err);
toast.error(err.message);
toast(
"Could not find default configuration. Please select a machine preset",
);
return undefined;
} finally {
setIsLoading(false);

View File

@ -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) => (
<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 {
vmConfig: VmConfig;
formHooks: UseFormReturn<VmConfig, any, undefined>;
setVmUuid: Dispatch<SetStateAction<string | null>>;
}
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<VmConfig> = 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"
>
<div className="col-span-4">
<ListSubheader>General</ListSubheader>
<ListSubheader sx={{ bgcolor: "inherit" }}>General</ListSubheader>
</div>
<VmPropLabel>Flake</VmPropLabel>
<VmPropContent>
<FlakeBadge flakeAttr={flake_attr} flakeUrl={flake_url} />
<FlakeBadge
flakeAttr={watch("flake_attr")}
flakeUrl={watch("flake_url")}
/>
</VmPropContent>
<VmPropLabel>Machine</VmPropLabel>
<VmPropContent>
<Controller
name="flake_attr"
control={control}
render={({ field }) => (
<Select {...field} variant="standard" fullWidth>
{["default", "vm1"].map((attr) => (
<MenuItem value={attr} key={attr}>
{attr}
</MenuItem>
))}
</Select>
)}
/>
{!isLoading && (
<Controller
name="flake_attr"
control={control}
render={({ field }) => (
<Select
{...field}
variant="standard"
fullWidth
disabled={isLoading}
>
{data?.data.flake_attrs.map((attr) => (
<MenuItem value={attr} key={attr}>
{attr}
</MenuItem>
))}
</Select>
)}
/>
)}
</VmPropContent>
<div className="col-span-4">
<ListSubheader>VM</ListSubheader>
<ListSubheader sx={{ bgcolor: "inherit" }}>VM</ListSubheader>
</div>
<VmPropLabel>CPU Cores</VmPropLabel>
<VmPropContent>
@ -103,7 +125,7 @@ export const ConfigureVM = (props: VmDetailsProps) => {
name="graphics"
control={control}
render={({ field }) => (
<Switch {...field} defaultChecked={vmConfig.graphics} />
<Switch {...field} defaultChecked={watch("graphics")} />
)}
/>
</VmPropContent>
@ -129,7 +151,12 @@ export const ConfigureVM = (props: VmDetailsProps) => {
<div className="col-span-4 grid items-center">
{isStarting && <LinearProgress />}
<Button type="submit" disabled={isStarting} variant="contained">
<Button
autoFocus
type="submit"
disabled={isStarting}
variant="contained"
>
Join Clan
</Button>
</div>

View File

@ -22,7 +22,7 @@ export const Confirm = (props: ConfirmProps) => {
return userConfirmed ? (
<ConfirmVM url={flakeUrl} handleBack={handleBack} />
) : (
<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 && (
<LoadingOverlay
title={"Loading Flake"}

View File

@ -26,10 +26,10 @@ export function ConfirmVM(props: ConfirmVMProps) {
const formHooks = useForm<VmConfig>({
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<string | null>(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) {
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2">
{!formState.isSubmitted && (
<>
{error && (
{/* {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">
{isLoading && (
<LoadingOverlay
@ -65,14 +64,10 @@ export function ConfirmVM(props: ConfirmVMProps) {
subtitle={<FlakeBadge flakeUrl={url} flakeAttr={url} />}
/>
)}
{config && (
<ConfigureVM
vmConfig={config}
formHooks={formHooks}
setVmUuid={setVmUuid}
/>
)}
{error && (
<ConfigureVM formHooks={formHooks} setVmUuid={setVmUuid} />
{/* {error && (
<>
<Button
color="error"
@ -93,7 +88,7 @@ export function ConfirmVM(props: ConfirmVMProps) {
}
/>
</>
)}
)} */}
</div>
</>
)}

View File

@ -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<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}
/>
)}
</Layout>
);
}