Merge pull request 'select modules' (#465) from feat/configure-modules into main
All checks were successful
assets1 / test (push) Successful in 44s
checks / test (push) Successful in 44s
checks-impure / test (push) Successful in 1m24s

This commit is contained in:
clan-bot 2023-11-04 13:04:27 +00:00
commit c9afa54c32
10 changed files with 207 additions and 78 deletions

View File

@ -6,6 +6,8 @@ import {
Button,
CssBaseline,
IconButton,
MenuItem,
Select,
ThemeProvider,
useMediaQuery,
} from "@mui/material";
@ -51,7 +53,7 @@ export default function RootLayout({
<title>Clan.lol</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Clan.lol - build your own network" />
<link rel="icon" href="favicon.ico" sizes="any" />
<link rel="icon" href="public/favicon.ico" sizes="any" />
</head>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
@ -70,10 +72,36 @@ export default function RootLayout({
<>
<Background />
<div className="flex h-screen overflow-hidden">
<Sidebar
show={showSidebarDerived}
onClose={() => setShowSidebar(false)}
/>
<ThemeProvider theme={darkTheme}>
<Sidebar
show={showSidebarDerived}
onClose={() => setShowSidebar(false)}
clanSelect={
appState.data.clanName && (
<Select
color="secondary"
label="clan"
fullWidth
variant="standard"
disableUnderline
value={appState.data.clanName}
onChange={(ev) => {
appState.setAppState((c) => ({
...c,
clanName: ev.target.value,
}));
}}
>
{appState.data.flakes?.map((clan) => (
<MenuItem value={clan} key={clan}>
{clan}
</MenuItem>
))}
</Select>
)
}
/>
</ThemeProvider>
<div
className={tw`${
!showSidebarDerived && translate

View File

@ -1,10 +1,18 @@
"use client";
import { useAppState } from "@/components/hooks/useAppContext";
import { MachineContextProvider } from "@/components/hooks/useMachines";
export default function Layout({ children }: { children: React.ReactNode }) {
const {
data: { clanName },
} = useAppState();
return (
// TODO: select flake?
<MachineContextProvider flakeName="defaultFlake">
{children}
</MachineContextProvider>
<>
{clanName && (
<MachineContextProvider flakeName={clanName}>
{children}
</MachineContextProvider>
)}
</>
);
}

View File

@ -0,0 +1,86 @@
import { setMachineSchema } from "@/api/machine/machine";
import { useListClanModules } from "@/api/modules/modules";
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import OutlinedInput from "@mui/material/OutlinedInput";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import { useEffect } from "react";
import { CreateMachineForm, FormStepContentProps } from "./interfaces";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
type ClanModulesProps = FormStepContentProps;
export default function ClanModules(props: ClanModulesProps) {
const { clanName, formHooks } = props;
const { data, isLoading } = useListClanModules(clanName);
const selectedModules = formHooks.watch("modules");
useEffect(() => {
setMachineSchema(clanName, "example_machine", {
imports: [],
}).then((response) => {
if (response.statusText == "OK") {
formHooks.setValue("schema", response.data.schema);
}
});
formHooks.setValue("modules", []);
}, [clanName, formHooks]);
const handleChange = (
event: SelectChangeEvent<CreateMachineForm["modules"]>,
) => {
const {
target: { value },
} = event;
const newValue = typeof value === "string" ? value.split(",") : value;
formHooks.setValue("modules", newValue);
setMachineSchema(clanName, "example_machine", {
imports: selectedModules,
}).then((response) => {
if (response.statusText == "OK") {
formHooks.setValue("schema", response.data.schema);
}
});
};
return (
<div>
<FormControl sx={{ m: 1, width: 300 }} disabled={isLoading}>
<InputLabel>Modules</InputLabel>
<Select
multiple
value={selectedModules}
onChange={handleChange}
input={<OutlinedInput label="Modules" />}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
MenuProps={MenuProps}
>
{data?.data.clan_modules.map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
}

View File

@ -1,10 +1,8 @@
"use client";
import { useGetMachineSchema } from "@/api/machine/machine";
import { Check, Error } from "@mui/icons-material";
import {
Box,
Button,
LinearProgress,
List,
ListItem,
ListItemIcon,
@ -22,41 +20,18 @@ import {
TranslatableString,
} from "@rjsf/utils";
import validator from "@rjsf/validator-ajv8";
import { JSONSchema7 } from "json-schema";
import { useMemo, useRef } from "react";
import toast from "react-hot-toast";
import { FormStepContentProps } from "./interfaces";
type ValueType = { default: any };
interface PureCustomConfigProps extends FormStepContentProps {
schema: JSONSchema7;
initialValues: any;
}
export function CustomConfig(props: FormStepContentProps) {
const { formHooks } = props;
const { data, isLoading, error } = useGetMachineSchema(
"defaultFlake",
"mama",
);
// const { data, isLoading, error } = { data: {data:{schema: {
// title: 'Test form',
// type: 'object',
// properties: {
// name: {
// type: 'string',
// },
// age: {
// type: 'number',
// },
// },
// }}}, isLoading: false, error: undefined }
const schema = useMemo(() => {
if (!isLoading && !error?.message && data?.data) {
return data?.data.schema;
}
return {};
}, [data, isLoading, error]);
const { formHooks, clanName } = props;
const schema = formHooks.watch("schema");
type ValueType = { default: any };
const initialValues = useMemo(
() =>
Object.entries(schema?.properties || {}).reduce((acc, [key, value]) => {
@ -72,15 +47,11 @@ export function CustomConfig(props: FormStepContentProps) {
[schema],
);
return isLoading ? (
<LinearProgress variant="indeterminate" />
) : error?.message ? (
<div>{error?.message}</div>
) : (
return (
<PureCustomConfig
clanName={clanName}
formHooks={formHooks}
initialValues={initialValues}
schema={schema}
/>
);
}
@ -115,9 +86,9 @@ function ErrorList<
}
function PureCustomConfig(props: PureCustomConfigProps) {
const { schema, formHooks } = props;
const { formHooks } = props;
const { setValue, watch } = formHooks;
const schema = watch("schema");
console.log({ schema });
const configData = watch("config") as IChangeEvent<any>;
@ -125,7 +96,6 @@ function PureCustomConfig(props: PureCustomConfigProps) {
console.log({ configData });
const setConfig = (data: IChangeEvent<any>) => {
console.log({ data });
setValue("config", data);
};

View File

@ -1,6 +1,7 @@
import {
Box,
Button,
LinearProgress,
MobileStepper,
Step,
StepLabel,
@ -10,14 +11,20 @@ import {
} from "@mui/material";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { useAppState } from "../hooks/useAppContext";
import ClanModules from "./clanModules";
import { CustomConfig } from "./customConfig";
import { CreateMachineForm, FormStep } from "./interfaces";
export function CreateMachineForm() {
const {
data: { clanName },
} = useAppState();
const formHooks = useForm<CreateMachineForm>({
defaultValues: {
name: "",
config: {},
modules: [],
},
});
const { handleSubmit, reset } = formHooks;
@ -34,12 +41,20 @@ export function CreateMachineForm() {
{
id: "modules",
label: "Modules",
content: <div></div>,
content: clanName ? (
<ClanModules clanName={clanName} formHooks={formHooks} />
) : (
<LinearProgress />
),
},
{
id: "config",
label: "Customize",
content: <CustomConfig formHooks={formHooks} />,
content: clanName ? (
<CustomConfig formHooks={formHooks} clanName={clanName} />
) : (
<LinearProgress />
),
},
{
id: "save",

View File

@ -1,3 +1,4 @@
import { JSONSchema7 } from "json-schema";
import { ReactElement } from "react";
import { UseFormReturn } from "react-hook-form";
@ -6,6 +7,8 @@ export type StepId = "template" | "modules" | "config" | "save";
export type CreateMachineForm = {
name: string;
config: any;
modules: string[];
schema: JSONSchema7;
};
export type FormHooks = UseFormReturn<CreateMachineForm>;
@ -18,6 +21,7 @@ export type FormStep = {
export interface FormStepContentProps {
formHooks: FormHooks;
clanName: string;
}
export type FormStepContent = ReactElement<FormStepContentProps>;

View File

@ -1,11 +1,15 @@
import { AxiosError } from "axios";
import { useListAllFlakes } from "@/api/flake/flake";
import { FlakeListResponse } from "@/api/model";
import { AxiosError, AxiosResponse } from "axios";
import React, {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useEffect,
useState,
} from "react";
import { KeyedMutator } from "swr";
type AppContextType = {
// data: AxiosResponse<{}, any> | undefined;
@ -15,19 +19,16 @@ type AppContextType = {
error: AxiosError<any> | undefined;
setAppState: Dispatch<SetStateAction<AppState>>;
// mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
mutate: KeyedMutator<AxiosResponse<FlakeListResponse, 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;
flakes?: FlakeListResponse["flakes"];
};
interface AppContextProviderProps {
@ -35,14 +36,33 @@ interface AppContextProviderProps {
}
export const WithAppState = (props: AppContextProviderProps) => {
const { children } = props;
const { isLoading, error, swrKey } = {
isLoading: false,
error: undefined,
swrKey: "default",
};
const {
isLoading,
error,
swrKey,
data: flakesResponse,
mutate,
} = useListAllFlakes({
swr: {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
});
const [data, setAppState] = useState<AppState>({ isJoined: false });
useEffect(() => {
if (!isLoading && !error && flakesResponse) {
const {
data: { flakes },
} = flakesResponse;
if (flakes.length >= 1) {
setAppState((c) => ({ ...c, clanName: flakes[0], isJoined: true }));
}
setAppState((c) => ({ ...c, flakes }));
}
}, [flakesResponse, error, isLoading]);
return (
<AppContext.Provider
value={{
@ -51,7 +71,7 @@ export const WithAppState = (props: AppContextProviderProps) => {
isLoading,
error,
swrKey,
// mutate,
mutate,
}}
>
{children}

View File

@ -34,7 +34,6 @@ type MachineContextType =
swrKey: string | false | Record<any, any>;
}
| {
flakeName: string;
isLoading: true;
data: readonly [];
};
@ -44,13 +43,10 @@ const initialState = {
data: [],
} as const;
export function CreateMachineContext(flakeName: string) {
return useMemo(() => {
return createContext<MachineContextType>({
...initialState,
flakeName,
});
}, [flakeName]);
export function CreateMachineContext() {
return createContext<MachineContextType>({
...initialState,
});
}
interface MachineContextProviderProps {
@ -58,6 +54,8 @@ interface MachineContextProviderProps {
flakeName: string;
}
const MachineContext = CreateMachineContext();
export const MachineContextProvider = (props: MachineContextProviderProps) => {
const { children, flakeName } = props;
const {
@ -80,8 +78,6 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => {
return [];
}, [isLoading, error, isValidating, rawData, filters]);
const MachineContext = CreateMachineContext(flakeName);
return (
<MachineContext.Provider
value={{
@ -105,5 +101,4 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => {
);
};
export const useMachines = (flakeName: string) =>
React.useContext(CreateMachineContext(flakeName));
export const useMachines = () => React.useContext(MachineContext);

View File

@ -75,9 +75,10 @@ const showSidebar = tw`lg:translate-x-0`;
interface SidebarProps {
show: boolean;
onClose: () => void;
clanSelect: React.ReactNode;
}
export function Sidebar(props: SidebarProps) {
const { show, onClose } = props;
const { show, onClose, clanSelect } = props;
return (
<aside
@ -96,6 +97,7 @@ export function Sidebar(props: SidebarProps) {
/>
</div>
</div>
<div className="self-center">{clanSelect}</div>
<Divider
flexItem
className="mx-8 mb-4 mt-9 hidden bg-neutral-40 lg:block"

View File

@ -16,7 +16,8 @@ import { SearchBar } from "./searchBar";
import { StickySpeedDial } from "./stickySpeedDial";
export function NodeTable() {
const machines = useMachines("defaultFlake");
const { isLoading, data: machines } = useMachines();
const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
@ -26,12 +27,12 @@ export function NodeTable() {
const [filteredList, setFilteredList] = useState<readonly Machine[]>([]);
const tableData = useMemo(() => {
const tableData = machines.data.map((machine) => {
const tableData = machines.map((machine) => {
return { name: machine.name, status: machine.status };
});
setFilteredList(tableData);
return tableData;
}, [machines.data]);
}, [machines]);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@ -42,7 +43,7 @@ export function NodeTable() {
setPage(0);
};
if (machines.isLoading) {
if (isLoading) {
return (
<Grid
container