Merge pull request 'select modules' (#465) from feat/configure-modules into main
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
86
pkgs/ui/src/components/createMachineForm/clanModules.tsx
Normal file
86
pkgs/ui/src/components/createMachineForm/clanModules.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user