Befor fixing linting problem
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"ignorePatterns": ["**/src/api/*"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ set -xeuo pipefail
|
||||
# GITEA_TOKEN
|
||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "GITEA_TOKEN is not set"
|
||||
echo "Go to https://git.clan.lol/user/settings/applications and generate a token"
|
||||
echo "Go to https://gitea.gchq.icu/user/settings/applications and generate a token"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -22,8 +22,10 @@ nix build '.#ui' --out-link "$tmpdir/result"
|
||||
tar --transform 's,^\.,assets,' -czvf "$tmpdir/assets.tar.gz" -C "$tmpdir"/result/lib/node_modules/*/out .
|
||||
NAR_HASH=$(nix-prefetch-url --unpack file://<(cat "$tmpdir/assets.tar.gz"))
|
||||
|
||||
|
||||
url="https://git.clan.lol/api/packages/clan/generic/ui/$NAR_HASH/assets.tar.gz"
|
||||
owner=Luis
|
||||
package_name=consulting-website
|
||||
package_version=$NAR_HASH
|
||||
url="https://gitea.gchq.icu/api/packages/$owner/generic/$package_name/$package_version/assets.tar.gz"
|
||||
set +x
|
||||
curl --upload-file "$tmpdir/assets.tar.gz" -X PUT "$url?token=$GITEA_TOKEN"
|
||||
set -x
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,5 +0,0 @@
|
||||
import JoinPrequel from "@/views/joinPrequel";
|
||||
|
||||
export default function Page() {
|
||||
return <JoinPrequel />;
|
||||
}
|
||||
@@ -3,11 +3,10 @@ import { Sidebar } from "@/components/sidebar";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import {
|
||||
Button,
|
||||
CssBaseline,
|
||||
IconButton,
|
||||
ThemeProvider,
|
||||
useMediaQuery,
|
||||
useMediaQuery
|
||||
} from "@mui/material";
|
||||
import { StyledEngineProvider } from "@mui/material/styles";
|
||||
import axios from "axios";
|
||||
@@ -62,9 +61,7 @@ export default function RootLayout({
|
||||
<AppContext.Consumer>
|
||||
{(appState) => {
|
||||
const showSidebarDerived = Boolean(
|
||||
showSidebar &&
|
||||
!appState.isLoading &&
|
||||
appState.data.isJoined,
|
||||
showSidebar && !appState.isLoading,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -86,9 +83,7 @@ export default function RootLayout({
|
||||
hidden={true}
|
||||
onClick={() => setShowSidebar((c) => !c)}
|
||||
>
|
||||
{!showSidebar && appState.data.isJoined && (
|
||||
<MenuIcon />
|
||||
)}
|
||||
{!showSidebar && <MenuIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="col-span-1 block w-full bg-fixed text-center font-semibold dark:invert lg:hidden">
|
||||
@@ -105,21 +100,7 @@ export default function RootLayout({
|
||||
|
||||
<div className="px-1">
|
||||
<div className="relative flex h-full flex-1 flex-col">
|
||||
<main>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
appState.setAppState((s) => ({
|
||||
...s,
|
||||
isJoined: !s.isJoined,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Toggle Joined
|
||||
</Button>
|
||||
|
||||
{children}
|
||||
</main>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CreateMachineForm } from "@/components/createMachineForm";
|
||||
|
||||
export default function CreateMachine() {
|
||||
return <CreateMachineForm />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
interface DeviceEditProps {
|
||||
params: { name: string };
|
||||
}
|
||||
|
||||
export default function EditDevice(props: DeviceEditProps) {
|
||||
const {
|
||||
params: { name },
|
||||
} = props;
|
||||
return <div>{name}</div>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MachineContextProvider } from "@/components/hooks/useMachines";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <MachineContextProvider>{children}</MachineContextProvider>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { NodeTable } from "@/components/table";
|
||||
import { StrictMode } from "react";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<NodeTable />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,10 @@
|
||||
"use client";
|
||||
import { NetworkOverview } from "@/components/dashboard/NetworkOverview";
|
||||
import { RecentActivity } from "@/components/dashboard/activity";
|
||||
import { AppOverview } from "@/components/dashboard/appOverview";
|
||||
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 { LoadingOverlay } from "@/components/join/loadingOverlay";
|
||||
import JoinPrequel from "@/views/joinPrequel";
|
||||
|
||||
// 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} 2xl:col-span-1 ${sx}`}>
|
||||
// <div className={tw`row-span-2`}>
|
||||
// {children}
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// interface DashboardPanelProps {
|
||||
// children?: React.ReactNode;
|
||||
// }
|
||||
// const DashboardPanel = (props: DashboardPanelProps) => {
|
||||
// const { children } = props;
|
||||
// return (
|
||||
// <div className="col-span-full row-span-1 2xl:col-span-2">{children}</div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data, isLoading } = useAppState();
|
||||
const { isLoading } = useAppState();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid h-full place-items-center">
|
||||
@@ -48,26 +17,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!data.isJoined) {
|
||||
return <JoinPrequel />;
|
||||
}
|
||||
if (data.isJoined) {
|
||||
} else {
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<div className="grid w-full grid-flow-row grid-cols-3 gap-4">
|
||||
<div className="row-span-2">
|
||||
<NetworkOverview />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<AppOverview />
|
||||
</div>
|
||||
<div className="row-span-2">
|
||||
<RecentActivity />
|
||||
</div>
|
||||
<QuickActions />
|
||||
<Notifications />
|
||||
<TaskQueue />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
ChevronLeft,
|
||||
Delete,
|
||||
Edit,
|
||||
Group,
|
||||
Key,
|
||||
Settings,
|
||||
SettingsEthernet,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
// import { useListMachines } from "@/api/default/default";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [{ id: "1" }, { id: "2" }];
|
||||
}
|
||||
|
||||
function getTemplate(params: { id: string }) {
|
||||
// const res = await fetch(`https://.../posts/${params.id}`);
|
||||
return {
|
||||
short: `My Template ${params.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
interface TemplateDetailProps {
|
||||
params: { id: string };
|
||||
}
|
||||
export default function TemplateDetail({ params }: TemplateDetailProps) {
|
||||
// const { data, isLoading } = useListMachines();
|
||||
const details = getTemplate(params);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full">
|
||||
<Button
|
||||
color="secondary"
|
||||
LinkComponent={"a"}
|
||||
href="/templates"
|
||||
startIcon={<ChevronLeft />}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full border border-solid border-neutral-90 bg-neutral-98 shadow-sm shadow-neutral-60 dark:bg-paper-dark">
|
||||
<div className="flex w-full flex-col items-center justify-center xl:p-2">
|
||||
<Avatar className="m-1 h-20 w-20 bg-purple-40">
|
||||
<Typography variant="h5">N</Typography>
|
||||
</Avatar>
|
||||
<Typography variant="h6" className="text-purple-40">
|
||||
{details.short}
|
||||
</Typography>
|
||||
<div className="w-full">
|
||||
<List
|
||||
className="xl:px-4"
|
||||
sx={{
|
||||
".MuiListSubheader-root": {
|
||||
px: 0,
|
||||
},
|
||||
".MuiListItem-root": {
|
||||
px: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListSubheader>
|
||||
<Typography variant="caption">Details</Typography>
|
||||
</ListSubheader>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<SettingsEthernet />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="network" secondary="10.9.20.2" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Key />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="secrets" secondary={"< ...hidden >"} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Group />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="clans" secondary={"Boss clan.lol"} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Attachment />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Image"
|
||||
secondary={"/nix/store/12789-image-clan-lol"}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton onClick={handleClick}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
<Menu
|
||||
MenuListProps={{
|
||||
className: "m-2",
|
||||
}}
|
||||
id="image-menu"
|
||||
aria-labelledby="image-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<MenuItem>View</MenuItem>
|
||||
<MenuItem>Rebuild</MenuItem>
|
||||
<MenuItem>Delete</MenuItem>
|
||||
</Menu>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Group />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="nodes"
|
||||
secondary={"Dad's PC; Mum; Olaf; ... 3 more"}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full justify-evenly">
|
||||
<Button
|
||||
variant="text"
|
||||
className="w-full text-black dark:text-white"
|
||||
startIcon={<Edit />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button className="w-full text-red" startIcon={<Delete />}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { ChevronRight } from "@mui/icons-material";
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
const templates = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Office Preset",
|
||||
date: "12 May 2050",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Work",
|
||||
date: "30 Feb 2020",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Family",
|
||||
date: "1 Okt 2022",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Standard",
|
||||
date: "24 Jul 2021",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ImageOverview() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Typography variant="h4">Templates</Typography>
|
||||
<List className="w-full gap-y-4">
|
||||
{templates.map(({ id, name, date }, idx, all) => (
|
||||
<>
|
||||
<ListItem key={id}>
|
||||
<ListItemButton LinkComponent={"a"} href={`/templates/${id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar className="bg-purple-40">{name.slice(0, 1)}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={name} secondary={date} />
|
||||
<ListItemSecondaryAction>
|
||||
<ChevronRight />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{idx < all.length - 1 && <Divider flexItem className="mx-10" />}
|
||||
</>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { useAppState } from "./hooks/useAppContext";
|
||||
|
||||
export default function Background() {
|
||||
const { data, isLoading } = useAppState();
|
||||
const { isLoading } = useAppState();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -14,7 +14,7 @@ export default function Background() {
|
||||
"fixed -z-10 h-[100vh] w-[100vw] overflow-hidden opacity-10 blur-md dark:opacity-40"
|
||||
}
|
||||
>
|
||||
{(isLoading || !data.isJoined) && (
|
||||
{isLoading && (
|
||||
<>
|
||||
<Image
|
||||
className="dark:hidden"
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"use client";
|
||||
import { useGetMachineSchema } from "@/api/default/default";
|
||||
import { Check, Error } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { IChangeEvent } from "@rjsf/core";
|
||||
import { Form } from "@rjsf/mui";
|
||||
import {
|
||||
ErrorListProps,
|
||||
FormContextType,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
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";
|
||||
|
||||
interface PureCustomConfigProps extends FormStepContentProps {
|
||||
schema: JSONSchema7;
|
||||
initialValues: any;
|
||||
}
|
||||
export function CustomConfig(props: FormStepContentProps) {
|
||||
const { formHooks } = props;
|
||||
const { data, isLoading, error } = useGetMachineSchema("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 initialValues = useMemo(
|
||||
() =>
|
||||
Object.entries(schema?.properties || {}).reduce((acc, [key, value]) => {
|
||||
/*@ts-ignore*/
|
||||
const init: any = value?.default;
|
||||
if (init) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: init,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
[schema],
|
||||
);
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress variant="indeterminate" />
|
||||
) : error?.message ? (
|
||||
<div>{error?.message}</div>
|
||||
) : (
|
||||
<PureCustomConfig
|
||||
formHooks={formHooks}
|
||||
initialValues={initialValues}
|
||||
schema={schema}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorList<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({ errors, registry }: ErrorListProps<T, S, F>) {
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<Paper elevation={0}>
|
||||
<Box mb={2} p={2}>
|
||||
<Typography variant="h6">
|
||||
{translateString(TranslatableString.ErrorsLabel)}
|
||||
</Typography>
|
||||
<List dense={true}>
|
||||
{errors.map((error, i: number) => {
|
||||
return (
|
||||
<ListItem key={i}>
|
||||
<ListItemIcon>
|
||||
<Error color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={error.stack} />
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function PureCustomConfig(props: PureCustomConfigProps) {
|
||||
const { schema, formHooks } = props;
|
||||
const { setValue, watch } = formHooks;
|
||||
|
||||
console.log({ schema });
|
||||
|
||||
const configData = watch("config") as IChangeEvent<any>;
|
||||
|
||||
console.log({ configData });
|
||||
|
||||
const setConfig = (data: IChangeEvent<any>) => {
|
||||
console.log({ data });
|
||||
setValue("config", data);
|
||||
};
|
||||
|
||||
const formRef = useRef<any>();
|
||||
|
||||
const validate = () => {
|
||||
const isValid: boolean = formRef?.current?.validateForm();
|
||||
console.log({ isValid }, formRef.current);
|
||||
if (!isValid) {
|
||||
formHooks.setError("config", {
|
||||
message: "invalid config",
|
||||
});
|
||||
toast.error(
|
||||
"Configuration is invalid. Please check the highlighted fields for details.",
|
||||
);
|
||||
} else {
|
||||
formHooks.clearErrors("config");
|
||||
toast.success("Config seems valid");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
ref={formRef}
|
||||
onChange={setConfig}
|
||||
formData={configData.formData}
|
||||
acceptcharset="utf-8"
|
||||
schema={schema}
|
||||
validator={validator}
|
||||
liveValidate={true}
|
||||
templates={{
|
||||
// ObjectFieldTemplate:
|
||||
ErrorListTemplate: ErrorList,
|
||||
ButtonTemplates: {
|
||||
SubmitButton: (props) => (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Button
|
||||
onClick={validate}
|
||||
startIcon={<Check />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Validate
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
MobileStepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CustomConfig } from "./customConfig";
|
||||
import { CreateMachineForm, FormStep } from "./interfaces";
|
||||
|
||||
export function CreateMachineForm() {
|
||||
const formHooks = useForm<CreateMachineForm>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
const { handleSubmit, reset } = formHooks;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [activeStep, setActiveStep] = useState<number>(0);
|
||||
|
||||
const steps: FormStep[] = [
|
||||
{
|
||||
id: "template",
|
||||
label: "Template",
|
||||
content: <div></div>,
|
||||
},
|
||||
{
|
||||
id: "modules",
|
||||
label: "Modules",
|
||||
content: <div></div>,
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "Customize",
|
||||
content: <CustomConfig formHooks={formHooks} />,
|
||||
},
|
||||
{
|
||||
id: "save",
|
||||
label: "Save",
|
||||
content: <div></div>,
|
||||
},
|
||||
];
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep < steps.length - 1) {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (activeStep > 0) {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
reset();
|
||||
};
|
||||
const currentStep = steps.at(activeStep);
|
||||
|
||||
async function onSubmit(data: any) {
|
||||
console.log({ data }, "Aggregated Data; creating machine from");
|
||||
}
|
||||
|
||||
const BackButton = () => (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
);
|
||||
|
||||
const NextButton = () => (
|
||||
<>
|
||||
{activeStep !== steps.length - 1 && (
|
||||
<Button
|
||||
disabled={!formHooks.formState.isValid}
|
||||
onClick={handleNext}
|
||||
color="secondary"
|
||||
>
|
||||
{activeStep <= steps.length - 1 && "Next"}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep === steps.length - 1 && (
|
||||
<Button color="secondary" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{isMobile && (
|
||||
<MobileStepper
|
||||
activeStep={activeStep}
|
||||
color="secondary"
|
||||
backButton={<BackButton />}
|
||||
nextButton={<NextButton />}
|
||||
steps={steps.length}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<Stepper activeStep={activeStep} color="secondary">
|
||||
{steps.map(({ label }, index) => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
const labelProps: {
|
||||
optional?: React.ReactNode;
|
||||
} = {};
|
||||
return (
|
||||
<Step
|
||||
sx={{
|
||||
".MuiStepIcon-root.Mui-active": {
|
||||
color: "secondary.main",
|
||||
},
|
||||
".MuiStepIcon-root.Mui-completed": {
|
||||
color: "secondary.main",
|
||||
},
|
||||
}}
|
||||
key={label}
|
||||
{...stepProps}
|
||||
>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
)}
|
||||
{/* <CustomConfig formHooks={formHooks} /> */}
|
||||
{/* The step Content */}
|
||||
{currentStep && currentStep.content}
|
||||
|
||||
{/* Desktop step controls */}
|
||||
{!isMobile && (
|
||||
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
|
||||
<BackButton />
|
||||
<Box sx={{ flex: "1 1 auto" }} />
|
||||
<NextButton />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ReactElement } from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
export type StepId = "template" | "modules" | "config" | "save";
|
||||
|
||||
export type CreateMachineForm = {
|
||||
name: string;
|
||||
config: any;
|
||||
};
|
||||
|
||||
export type FormHooks = UseFormReturn<CreateMachineForm>;
|
||||
|
||||
export type FormStep = {
|
||||
id: StepId;
|
||||
label: string;
|
||||
content: FormStepContent;
|
||||
};
|
||||
|
||||
export interface FormStepContentProps {
|
||||
formHooks: FormHooks;
|
||||
}
|
||||
|
||||
export type FormStepContent = ReactElement<FormStepContentProps>;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { DashboardCard } from "@/components/card";
|
||||
import { NoDataOverlay } from "@/components/noDataOverlay";
|
||||
import { status, Status, clanStatus } from "@/data/dashboardData";
|
||||
import {
|
||||
Chip,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
const statusColorMap: Record<
|
||||
Status,
|
||||
"default" | "primary" | "secondary" | "error" | "info" | "success" | "warning"
|
||||
> = {
|
||||
online: "info",
|
||||
offline: "error",
|
||||
pending: "default",
|
||||
};
|
||||
|
||||
const MAX_OTHERS = 5;
|
||||
|
||||
export const NetworkOverview = () => {
|
||||
const { self, other } = clanStatus;
|
||||
|
||||
const firstOthers = other.slice(0, MAX_OTHERS);
|
||||
return (
|
||||
<DashboardCard title="Clan Overview">
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary={self.name} secondary={self.status} />
|
||||
<ListItemIcon>
|
||||
<Chip
|
||||
label={status[self.status]}
|
||||
color={statusColorMap[self.status]}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
<Divider flexItem />
|
||||
{!other.length && (
|
||||
<div className="my-3 flex h-full w-full justify-center align-middle">
|
||||
<NoDataOverlay
|
||||
label={
|
||||
<ListItemText
|
||||
primary="No other nodes"
|
||||
secondary={<Link href="/nodes">Add devices</Link>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{firstOthers.map((o) => (
|
||||
<ListItem key={o.id}>
|
||||
<ListItemText primary={o.name} secondary={o.status} />
|
||||
<ListItemIcon>
|
||||
<Chip label={status[o.status]} color={statusColorMap[o.status]} />
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
))}
|
||||
{other.length > MAX_OTHERS && (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
secondary={` ${other.length - MAX_OTHERS} more ...`}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import { DashboardCard } from "@/components/card";
|
||||
import Image from "next/image";
|
||||
interface AppCardProps {
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
const AppCard = (props: AppCardProps) => {
|
||||
const { name, icon } = props;
|
||||
const iconPath = icon
|
||||
? `/app-icons/${icon}`
|
||||
: "app-icons/app-placeholder.svg";
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
className="flex h-40 w-40 cursor-pointer items-center justify-center rounded-3xl p-2
|
||||
align-middle shadow-md ring-2 ring-inset ring-purple-50
|
||||
hover:bg-neutral-90 focus:bg-neutral-90 active:bg-neutral-80
|
||||
dark:hover:bg-neutral-10 dark:focus:bg-neutral-10 dark:active:bg-neutral-20"
|
||||
>
|
||||
<div className="flex w-full flex-col justify-center">
|
||||
<div className="my-1 flex h-[22] w-[22] items-center justify-center self-center overflow-visible p-1 dark:invert">
|
||||
<Image
|
||||
src={iconPath}
|
||||
alt={`${name}-app-icon`}
|
||||
width={18 * 3}
|
||||
height={18 * 3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center">{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const apps = [
|
||||
{
|
||||
name: "Firefox",
|
||||
icon: "firefox.svg",
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
icon: "discord.svg",
|
||||
},
|
||||
{
|
||||
name: "Docs",
|
||||
},
|
||||
{
|
||||
name: "Dochub",
|
||||
icon: "dochub.svg",
|
||||
},
|
||||
{
|
||||
name: "Chess",
|
||||
icon: "chess.svg",
|
||||
},
|
||||
{
|
||||
name: "Games",
|
||||
icon: "games.svg",
|
||||
},
|
||||
{
|
||||
name: "Mail",
|
||||
icon: "mail.svg",
|
||||
},
|
||||
{
|
||||
name: "Public transport",
|
||||
icon: "public-transport.svg",
|
||||
},
|
||||
{
|
||||
name: "Outlook",
|
||||
icon: "mail.svg",
|
||||
},
|
||||
{
|
||||
name: "Youtube",
|
||||
icon: "youtube.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export const AppOverview = () => {
|
||||
return (
|
||||
<DashboardCard title="Applications">
|
||||
<div className="flex h-full w-full justify-center">
|
||||
<div className="flex h-full w-fit justify-center">
|
||||
<div className="grid w-full auto-cols-min auto-rows-min grid-cols-2 gap-8 py-8 sm:grid-cols-3 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 ">
|
||||
{apps.map((app) => (
|
||||
<AppCard key={app.name} name={app.name} icon={app.icon} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { DashboardCard } from "@/components/card";
|
||||
import { notificationData } from "@/data/dashboardData";
|
||||
import {
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import PriorityHighIcon from "@mui/icons-material/PriorityHigh";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
const severityMap = {
|
||||
info: {
|
||||
icon: <InfoIcon />,
|
||||
color: "info",
|
||||
},
|
||||
success: {
|
||||
icon: <CheckIcon />,
|
||||
color: "success",
|
||||
},
|
||||
warning: {
|
||||
icon: <PriorityHighIcon />,
|
||||
color: "warning",
|
||||
},
|
||||
error: {
|
||||
icon: <CloseIcon />,
|
||||
color: "error",
|
||||
},
|
||||
};
|
||||
|
||||
export const Notifications = () => {
|
||||
return (
|
||||
<DashboardCard title="Notifications">
|
||||
<List>
|
||||
{notificationData.map((n, idx) => (
|
||||
<ListItem key={idx}>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: `${n.severity}.main`,
|
||||
}}
|
||||
>
|
||||
{severityMap[n.severity].icon}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={n.msg}
|
||||
secondary={n.date}
|
||||
sx={{
|
||||
width: "100px",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={n.source}
|
||||
sx={{
|
||||
width: "100px",
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
import { DashboardCard } from "@/components/card";
|
||||
import { Fab, Typography } from "@mui/material";
|
||||
import { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
import AppsIcon from "@mui/icons-material/Apps";
|
||||
import DevicesIcon from "@mui/icons-material/Devices";
|
||||
import LanIcon from "@mui/icons-material/Lan";
|
||||
|
||||
type Action = {
|
||||
id: string;
|
||||
icon: ReactNode;
|
||||
label: ReactNode;
|
||||
eventHandler: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const QuickActions = () => {
|
||||
const actions: Action[] = [
|
||||
{
|
||||
id: "network",
|
||||
icon: <LanIcon sx={{ mr: 1 }} />,
|
||||
label: "Network",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "apps",
|
||||
icon: <AppsIcon sx={{ mr: 1 }} />,
|
||||
label: "Apps",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "nodes",
|
||||
icon: <DevicesIcon sx={{ mr: 1 }} />,
|
||||
label: "Devices",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<DashboardCard title="Quick Actions">
|
||||
<div className="flex h-full w-full items-center justify-start pb-10 align-bottom">
|
||||
<div className="flex w-full flex-col flex-wrap justify-evenly gap-2 sm:flex-row">
|
||||
{actions.map(({ id, icon, label, eventHandler }) => (
|
||||
<Fab
|
||||
className="w-fit self-center shadow-none"
|
||||
color="secondary"
|
||||
key={id}
|
||||
onClick={eventHandler}
|
||||
variant="extended"
|
||||
>
|
||||
{icon}
|
||||
<Typography>{label}</Typography>
|
||||
</Fab>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { DashboardCard } from "@/components/card";
|
||||
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import ScheduleIcon from "@mui/icons-material/Schedule";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import { ReactNode } from "react";
|
||||
import { Chip } from "@mui/material";
|
||||
|
||||
const statusMap = {
|
||||
running: <SyncIcon className="animate-bounce" />,
|
||||
done: <DoneIcon />,
|
||||
planned: <ScheduleIcon />,
|
||||
};
|
||||
|
||||
interface TaskEntryProps {
|
||||
status: ReactNode;
|
||||
result: "default" | "error" | "info" | "success" | "warning";
|
||||
task: string;
|
||||
details?: string;
|
||||
}
|
||||
const TaskEntry = (props: TaskEntryProps) => {
|
||||
const { result, task, status } = props;
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">{status}</div>
|
||||
<div className="col-span-4">{task}</div>
|
||||
<div className="col-span-1">
|
||||
<Chip color={result} label={result} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TaskQueue = () => {
|
||||
return (
|
||||
<DashboardCard title="Task Queue">
|
||||
<div className="grid grid-cols-6 gap-2 p-4">
|
||||
<TaskEntry
|
||||
result="success"
|
||||
task="Update DevX"
|
||||
status={statusMap.done}
|
||||
/>
|
||||
<TaskEntry
|
||||
result="default"
|
||||
task="Update XYZ"
|
||||
status={statusMap.running}
|
||||
/>
|
||||
<TaskEntry
|
||||
result="default"
|
||||
task="Update ABC"
|
||||
status={statusMap.planned}
|
||||
/>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Chip } from "@mui/material";
|
||||
|
||||
interface FlakeBadgeProps {
|
||||
flakeUrl: string;
|
||||
flakeAttr: string;
|
||||
}
|
||||
export const FlakeBadge = (props: FlakeBadgeProps) => (
|
||||
<Chip
|
||||
color="secondary"
|
||||
label={`${props.flakeUrl}#${props.flakeAttr}`}
|
||||
sx={{
|
||||
p: 2,
|
||||
"&.MuiChip-root": {
|
||||
maxWidth: "unset",
|
||||
},
|
||||
"&.MuiChip-label": {
|
||||
overflow: "unset",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -11,36 +11,28 @@ import 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;
|
||||
};
|
||||
type AppState = {};
|
||||
|
||||
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 });
|
||||
const isLoading = false;
|
||||
const error = undefined;
|
||||
|
||||
const [data, setAppState] = useState<AppState>({});
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
@@ -49,8 +41,6 @@ export const WithAppState = (props: AppContextProviderProps) => {
|
||||
setAppState,
|
||||
isLoading,
|
||||
error,
|
||||
swrKey,
|
||||
mutate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useListMachines } from "@/api/default/default";
|
||||
import { Machine, MachinesResponse } from "@/api/model";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import React, {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { KeyedMutator } from "swr";
|
||||
|
||||
type Filter = {
|
||||
name: keyof Machine;
|
||||
value: Machine[keyof Machine];
|
||||
};
|
||||
type Filters = Filter[];
|
||||
|
||||
type MachineContextType =
|
||||
| {
|
||||
rawData: AxiosResponse<MachinesResponse, any> | undefined;
|
||||
data: Machine[];
|
||||
isLoading: boolean;
|
||||
error: AxiosError<any> | undefined;
|
||||
isValidating: boolean;
|
||||
|
||||
filters: Filters;
|
||||
setFilters: Dispatch<SetStateAction<Filters>>;
|
||||
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
|
||||
swrKey: string | false | Record<any, any>;
|
||||
}
|
||||
| {
|
||||
isLoading: true;
|
||||
data: readonly [];
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
isLoading: true,
|
||||
data: [],
|
||||
} as const;
|
||||
|
||||
export const MachineContext = createContext<MachineContextType>(initialState);
|
||||
|
||||
interface MachineContextProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const MachineContextProvider = (props: MachineContextProviderProps) => {
|
||||
const { children } = props;
|
||||
const {
|
||||
data: rawData,
|
||||
isLoading,
|
||||
error,
|
||||
isValidating,
|
||||
mutate,
|
||||
swrKey,
|
||||
} = useListMachines();
|
||||
const [filters, setFilters] = useState<Filters>([]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!isLoading && !error && !isValidating && rawData) {
|
||||
const { machines } = rawData.data;
|
||||
return machines.filter((m) =>
|
||||
filters.every((f) => m[f.name] === f.value),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}, [isLoading, error, isValidating, rawData, filters]);
|
||||
|
||||
return (
|
||||
<MachineContext.Provider
|
||||
value={{
|
||||
rawData,
|
||||
data,
|
||||
|
||||
isLoading,
|
||||
error,
|
||||
isValidating,
|
||||
|
||||
filters,
|
||||
setFilters,
|
||||
|
||||
swrKey,
|
||||
mutate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MachineContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMachines = () => React.useContext(MachineContext);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { inspectVm } from "@/api/default/default";
|
||||
import { HTTPValidationError, VmConfig } from "@/api/model";
|
||||
import { AxiosError } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
interface UseVmsOptions {
|
||||
url: string;
|
||||
attr: string;
|
||||
}
|
||||
export const useVms = (options: UseVmsOptions) => {
|
||||
const { url, attr } = options;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [config, setConfig] = useState<VmConfig>();
|
||||
const [error, setError] = useState<AxiosError<HTTPValidationError>>();
|
||||
|
||||
useEffect(() => {
|
||||
const getVmInfo = async (url: string, attr: string) => {
|
||||
if (url === "" || !url) {
|
||||
toast.error("Flake url is missing", { id: "missing.flake.url" });
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const response = await inspectVm({
|
||||
flake_attr: attr,
|
||||
flake_url: url,
|
||||
});
|
||||
const {
|
||||
data: { config },
|
||||
} = response;
|
||||
setError(undefined);
|
||||
return config;
|
||||
} catch (e) {
|
||||
const err = e as AxiosError<HTTPValidationError>;
|
||||
setError(err);
|
||||
toast(
|
||||
"Could not find default configuration. Please select a machine preset",
|
||||
);
|
||||
return undefined;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getVmInfo(url, attr).then((c) => setConfig(c));
|
||||
}, [url, attr]);
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
config,
|
||||
};
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
InputAdornment,
|
||||
LinearProgress,
|
||||
ListSubheader,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form";
|
||||
import { FlakeBadge } from "../flakeBadge/flakeBadge";
|
||||
import { createVm, useInspectFlakeAttrs } from "@/api/default/default";
|
||||
import { VmConfig } from "@/api/model";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useAppState } from "../hooks/useAppContext";
|
||||
|
||||
interface VmPropLabelProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const VmPropLabel = (props: VmPropLabelProps) => (
|
||||
<div className="col-span-4 flex items-center sm:col-span-1">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface VmPropContentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const VmPropContent = (props: VmPropContentProps) => (
|
||||
<div className="col-span-4 sm:col-span-3">{props.children}</div>
|
||||
);
|
||||
|
||||
interface VmDetailsProps {
|
||||
formHooks: UseFormReturn<VmConfig, any, undefined>;
|
||||
setVmUuid: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export const ConfigureVM = (props: VmDetailsProps) => {
|
||||
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);
|
||||
console.log(data);
|
||||
const response = await createVm(data);
|
||||
const { uuid } = response?.data || null;
|
||||
|
||||
setVmUuid(() => uuid);
|
||||
setStarting(false);
|
||||
if (response.statusText === "OK") {
|
||||
toast.success(("Joined @ " + uuid) as string);
|
||||
setAppState((s) => ({ ...s, isJoined: true }));
|
||||
} else {
|
||||
toast.error("Could not join");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-4 gap-y-10"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<ListSubheader sx={{ bgcolor: "inherit" }}>General</ListSubheader>
|
||||
</div>
|
||||
<VmPropLabel>Flake</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<FlakeBadge
|
||||
flakeAttr={watch("flake_attr")}
|
||||
flakeUrl={watch("flake_url")}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Machine</VmPropLabel>
|
||||
<VmPropContent>
|
||||
{!isLoading && (
|
||||
<Controller
|
||||
name="flake_attr"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...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}>
|
||||
{attr}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</VmPropContent>
|
||||
<div className="col-span-4">
|
||||
<ListSubheader sx={{ bgcolor: "inherit" }}>VM</ListSubheader>
|
||||
</div>
|
||||
<VmPropLabel>CPU Cores</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="cores"
|
||||
control={control}
|
||||
render={({ field }) => <TextField type="number" {...field} />}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Graphics</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="graphics"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch {...field} defaultChecked={watch("graphics")} />
|
||||
)}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Memory Size</VmPropLabel>
|
||||
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="memory_size"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="number"
|
||||
{...field}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">MiB</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</VmPropContent>
|
||||
|
||||
<div className="col-span-4 grid items-center">
|
||||
{isStarting && <LinearProgress />}
|
||||
<Button
|
||||
autoFocus
|
||||
type="submit"
|
||||
disabled={isStarting}
|
||||
variant="contained"
|
||||
>
|
||||
Join Clan
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { LoadingOverlay } from "./loadingOverlay";
|
||||
import { FlakeBadge } from "../flakeBadge/flakeBadge";
|
||||
import { Typography, Button } from "@mui/material";
|
||||
// import { FlakeResponse } from "@/api/model";
|
||||
import { ConfirmVM } from "./confirmVM";
|
||||
import { Log } from "./log";
|
||||
import GppMaybeIcon from "@mui/icons-material/GppMaybe";
|
||||
import { useInspectFlake } from "@/api/default/default";
|
||||
|
||||
interface ConfirmProps {
|
||||
flakeUrl: string;
|
||||
flakeAttr: string;
|
||||
handleBack: () => void;
|
||||
}
|
||||
export const Confirm = (props: ConfirmProps) => {
|
||||
const { flakeUrl, handleBack, flakeAttr } = props;
|
||||
const [userConfirmed, setUserConfirmed] = useState(false);
|
||||
|
||||
const { data, isLoading } = useInspectFlake({
|
||||
url: flakeUrl,
|
||||
});
|
||||
|
||||
return userConfirmed ? (
|
||||
<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 ">
|
||||
{isLoading && (
|
||||
<LoadingOverlay
|
||||
title={"Loading Flake"}
|
||||
subtitle={<FlakeBadge flakeUrl={flakeUrl} flakeAttr={flakeAttr} />}
|
||||
/>
|
||||
)}
|
||||
{data && (
|
||||
<>
|
||||
<Typography variant="subtitle1">
|
||||
To join the clan you must trust the Author
|
||||
</Typography>
|
||||
<GppMaybeIcon sx={{ height: "10rem", width: "10rem", mb: 5 }} />
|
||||
<Button
|
||||
autoFocus
|
||||
size="large"
|
||||
color="warning"
|
||||
variant="contained"
|
||||
onClick={() => setUserConfirmed(true)}
|
||||
sx={{ mb: 10 }}
|
||||
>
|
||||
Trust Flake Author
|
||||
</Button>
|
||||
<Log
|
||||
title="What's about to be built"
|
||||
lines={data.data.content.split("\n")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { VmConfig } from "@/api/model";
|
||||
import { useVms } from "@/components/hooks/useVms";
|
||||
|
||||
import { LoadingOverlay } from "./loadingOverlay";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ConfigureVM } from "./configureVM";
|
||||
import { VmBuildLogs } from "./vmBuildLogs";
|
||||
|
||||
interface ConfirmVMProps {
|
||||
url: string;
|
||||
handleBack: () => void;
|
||||
defaultFlakeAttr: string;
|
||||
}
|
||||
|
||||
export function ConfirmVM(props: ConfirmVMProps) {
|
||||
const { url, defaultFlakeAttr } = props;
|
||||
const formHooks = useForm<VmConfig>({
|
||||
defaultValues: {
|
||||
flake_url: url,
|
||||
flake_attr: defaultFlakeAttr,
|
||||
cores: 4,
|
||||
graphics: true,
|
||||
memory_size: 2048,
|
||||
},
|
||||
});
|
||||
const [vmUuid, setVmUuid] = useState<string | null>(null);
|
||||
|
||||
const { setValue, watch, formState } = formHooks;
|
||||
const { config, isLoading } = useVms({
|
||||
url,
|
||||
attr: watch("flake_attr") || defaultFlakeAttr,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setValue("cores", config?.cores);
|
||||
setValue("memory_size", config?.memory_size);
|
||||
setValue("graphics", config?.graphics);
|
||||
}
|
||||
}, [config, setValue]);
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2">
|
||||
{!formState.isSubmitted && (
|
||||
<>
|
||||
<div className="mb-2 w-full max-w-2xl">
|
||||
{isLoading && (
|
||||
<LoadingOverlay title={"Loading VM Configuration"} subtitle="" />
|
||||
)}
|
||||
|
||||
<ConfigureVM formHooks={formHooks} setVmUuid={setVmUuid} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formState.isSubmitted && vmUuid && <VmBuildLogs vmUuid={vmUuid} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
import { Typography } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
export const Layout = (props: LayoutProps) => {
|
||||
return (
|
||||
<div className="grid h-[70vh] w-full grid-cols-1 justify-center gap-y-4">
|
||||
<Typography variant="h4" className="w-full text-center">
|
||||
Join{" "}
|
||||
<Typography variant="h4" className="font-bold" component={"span"}>
|
||||
Clan.lol
|
||||
</Typography>
|
||||
</Typography>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,14 +11,9 @@ import Image from "next/image";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { tw } from "@/utils/tailwind";
|
||||
import AppsIcon from "@mui/icons-material/Apps";
|
||||
import BackupIcon from "@mui/icons-material/Backup";
|
||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
||||
import DesignServicesIcon from "@mui/icons-material/DesignServices";
|
||||
import DevicesIcon from "@mui/icons-material/Devices";
|
||||
import LanIcon from "@mui/icons-material/Lan";
|
||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||
import Link from "next/link";
|
||||
|
||||
import WysiwygIcon from "@mui/icons-material/Wysiwyg";
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
|
||||
type MenuEntry = {
|
||||
@@ -32,39 +27,15 @@ type MenuEntry = {
|
||||
|
||||
const menuEntries: MenuEntry[] = [
|
||||
{
|
||||
icon: <DashboardIcon />,
|
||||
label: "Dashoard",
|
||||
icon: <AssignmentIndIcon />,
|
||||
label: "Freelance",
|
||||
to: "/",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <DevicesIcon />,
|
||||
label: "Machines",
|
||||
to: "/machines",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <AppsIcon />,
|
||||
label: "Applications",
|
||||
to: "/applications",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: <LanIcon />,
|
||||
label: "Network",
|
||||
to: "/network",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: <DesignServicesIcon />,
|
||||
label: "Templates",
|
||||
to: "/templates",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <BackupIcon />,
|
||||
label: "Backups",
|
||||
to: "/backups",
|
||||
icon: <WysiwygIcon />,
|
||||
label: "Blog",
|
||||
to: "/blog",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
@@ -138,23 +109,6 @@ export function Sidebar(props: SidebarProps) {
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider
|
||||
flexItem
|
||||
className="mx-8 my-10 hidden bg-neutral-40 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">
|
||||
Clan.lol Admin
|
||||
</h3>
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
className="inline-block w-full rounded-md p-2 text-center text-white hover:text-purple-60/95"
|
||||
>
|
||||
Donate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid2 from "@mui/material/Unstable_Grid2";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
import { PieCards } from "./pieCards";
|
||||
import { PieData, NodePieChart } from "./nodePieChart";
|
||||
import { Machine } from "@/api/model/machine";
|
||||
import { Status } from "@/api/model";
|
||||
|
||||
interface EnhancedTableToolbarProps {
|
||||
tableData: readonly Machine[];
|
||||
}
|
||||
|
||||
export function EnhancedTableToolbar(
|
||||
props: React.PropsWithChildren<EnhancedTableToolbarProps>,
|
||||
) {
|
||||
const { tableData } = props;
|
||||
const theme = useTheme();
|
||||
const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
|
||||
|
||||
const pieData: PieData[] = useMemo(() => {
|
||||
const online = tableData.filter(
|
||||
(row) => row.status === Status.online,
|
||||
).length;
|
||||
const offline = tableData.filter(
|
||||
(row) => row.status === Status.offline,
|
||||
).length;
|
||||
const pending = tableData.filter(
|
||||
(row) => row.status === Status.unknown,
|
||||
).length;
|
||||
|
||||
return [
|
||||
{ name: "Online", value: online, color: theme.palette.success.main },
|
||||
{ name: "Offline", value: offline, color: theme.palette.error.main },
|
||||
{ name: "Pending", value: pending, color: theme.palette.warning.main },
|
||||
];
|
||||
}, [tableData, theme]);
|
||||
|
||||
return (
|
||||
<Grid2 container spacing={1}>
|
||||
{/* Pie Chart Grid */}
|
||||
<Grid2
|
||||
key="PieChart"
|
||||
md={6}
|
||||
xs={12}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box height={350} width={400}>
|
||||
<NodePieChart data={pieData} showLabels={is_lg} />
|
||||
</Box>
|
||||
</Grid2>
|
||||
|
||||
{/* Card Stack Grid */}
|
||||
<Grid2
|
||||
key="CardStack"
|
||||
lg={6}
|
||||
display="flex"
|
||||
sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
|
||||
>
|
||||
<PieCards pieData={pieData} />
|
||||
</Grid2>
|
||||
|
||||
{/*Toolbar Grid */}
|
||||
<Grid2
|
||||
key="Toolbar"
|
||||
xs={12}
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ pl: { sm: 2 }, pr: { xs: 1, sm: 1 }, pt: { xs: 1, sm: 3 } }}
|
||||
>
|
||||
{props.children}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { NodeTable } from "./nodeTable";
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from "react";
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export interface PieData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: PieData[];
|
||||
showLabels?: boolean;
|
||||
}
|
||||
|
||||
export function NodePieChart(props: Props) {
|
||||
const theme = useTheme();
|
||||
const { data, showLabels } = props;
|
||||
|
||||
return (
|
||||
<Box height={350}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
innerRadius={85}
|
||||
outerRadius={120}
|
||||
fill={theme.palette.primary.main}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
label={showLabels}
|
||||
legendType="square"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
startAngle={0}
|
||||
endAngle={360}
|
||||
paddingAngle={0}
|
||||
labelLine={true}
|
||||
hide={false}
|
||||
minAngle={0}
|
||||
isAnimationActive={true}
|
||||
animationBegin={0}
|
||||
animationDuration={1000}
|
||||
animationEasing="ease-in"
|
||||
blendStroke={true}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend verticalAlign="bottom" />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import Stack from "@mui/material/Stack/Stack";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
|
||||
import { Collapse } from "@mui/material";
|
||||
import { Machine, Status } from "@/api/model";
|
||||
|
||||
function renderStatus(status: Status) {
|
||||
switch (status) {
|
||||
case Status.online:
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="success" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Online
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
case Status.offline:
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="error" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Offline
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
case Status.unknown:
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="warning" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Pending
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
export function NodeRow(props: {
|
||||
row: Machine;
|
||||
selected: string | undefined;
|
||||
setSelected: (a: string | undefined) => void;
|
||||
}) {
|
||||
const { row, selected, setSelected } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
// Speed optimization. We compare string pointers here instead of the string content.
|
||||
const isSelected = selected == row.name;
|
||||
|
||||
const handleClick = (event: React.MouseEvent<unknown>, name: string) => {
|
||||
if (isSelected) {
|
||||
setSelected(undefined);
|
||||
} else {
|
||||
setSelected(name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Rendered Row */}
|
||||
<TableRow
|
||||
hover
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}
|
||||
tabIndex={-1}
|
||||
key={row.name}
|
||||
selected={isSelected}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<TableCell padding="none">
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
onClick={(event) => handleClick(event, row.name)}
|
||||
>
|
||||
<Stack>
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
{row.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
onClick={(event) => handleClick(event, row.name)}
|
||||
>
|
||||
{renderStatus(row.status)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Row Expansion */}
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ margin: 1 }}>
|
||||
<Typography variant="h6" gutterBottom component="div">
|
||||
Metadata
|
||||
</Typography>
|
||||
<Grid2 container spacing={2} paddingLeft={0}>
|
||||
<Grid2
|
||||
xs={6}
|
||||
justifyContent="left"
|
||||
display="flex"
|
||||
paddingRight={3}
|
||||
>
|
||||
<Box>Hello1</Box>
|
||||
</Grid2>
|
||||
<Grid2 xs={6} paddingLeft={6}>
|
||||
<Box>Hello2</Box>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CircularProgress, Grid, useTheme } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import TablePagination from "@mui/material/TablePagination";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { ChangeEvent, useMemo, useState } from "react";
|
||||
|
||||
import { Machine } from "@/api/model/machine";
|
||||
import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
|
||||
import { useMachines } from "../hooks/useMachines";
|
||||
import { EnhancedTableToolbar } from "./enhancedTableToolbar";
|
||||
import { NodeTableContainer } from "./nodeTableContainer";
|
||||
import { SearchBar } from "./searchBar";
|
||||
import { StickySpeedDial } from "./stickySpeedDial";
|
||||
|
||||
export function NodeTable() {
|
||||
const machines = useMachines();
|
||||
const theme = useTheme();
|
||||
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
|
||||
|
||||
const [selected, setSelected] = useState<string | undefined>(undefined);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [filteredList, setFilteredList] = useState<readonly Machine[]>([]);
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
const tableData = machines.data.map((machine) => {
|
||||
return { name: machine.name, status: machine.status };
|
||||
});
|
||||
setFilteredList(tableData);
|
||||
return tableData;
|
||||
}, [machines.data]);
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
if (machines.isLoading) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
h: "100vh",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={80} color="secondary" />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Paper sx={{ width: "100%", mb: 2 }}>
|
||||
<StickySpeedDial selected={selected} />
|
||||
<EnhancedTableToolbar tableData={tableData}>
|
||||
<Grid2 xs={12}>
|
||||
<SearchBar
|
||||
tableData={tableData}
|
||||
setFilteredList={setFilteredList}
|
||||
/>
|
||||
</Grid2>
|
||||
</EnhancedTableToolbar>
|
||||
|
||||
<NodeTableContainer
|
||||
tableData={filteredList}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
dense={false}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
|
||||
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
|
||||
component="div"
|
||||
count={filteredList.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { NodeRow } from "./nodeRow";
|
||||
|
||||
import { Machine } from "@/api/model/machine";
|
||||
|
||||
interface HeadCell {
|
||||
disablePadding: boolean;
|
||||
id: keyof Machine;
|
||||
label: string;
|
||||
alignRight: boolean;
|
||||
}
|
||||
|
||||
const headCells: readonly HeadCell[] = [
|
||||
{
|
||||
id: "name",
|
||||
alignRight: false,
|
||||
disablePadding: false,
|
||||
label: "DOMAIN NAME",
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
alignRight: false,
|
||||
disablePadding: false,
|
||||
label: "STATUS",
|
||||
},
|
||||
];
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export type NodeOrder = "asc" | "desc";
|
||||
|
||||
function getComparator<Key extends keyof any>(
|
||||
order: NodeOrder,
|
||||
orderBy: Key,
|
||||
): (
|
||||
a: { [key in Key]: number | string | boolean },
|
||||
b: { [key in Key]: number | string | boolean },
|
||||
) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
|
||||
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you
|
||||
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
|
||||
// with exampleArray.slice().sort(exampleComparator)
|
||||
function stableSort<T>(
|
||||
array: readonly T[],
|
||||
comparator: (a: T, b: T) => number,
|
||||
) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) {
|
||||
return order;
|
||||
}
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map((el) => el[0]);
|
||||
}
|
||||
|
||||
interface EnhancedTableProps {
|
||||
onRequestSort: (
|
||||
event: React.MouseEvent<unknown>,
|
||||
property: keyof Machine,
|
||||
) => void;
|
||||
order: NodeOrder;
|
||||
orderBy: string;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function EnhancedTableHead(props: EnhancedTableProps) {
|
||||
const { order, orderBy, onRequestSort } = props;
|
||||
const createSortHandler =
|
||||
(property: keyof Machine) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell id="dropdown" colSpan={1} />
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.alignRight ? "right" : "left"}
|
||||
padding={headCell.disablePadding ? "none" : "normal"}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : "asc"}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
||||
</Box>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
interface NodeTableContainerProps {
|
||||
tableData: readonly Machine[];
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
dense: boolean;
|
||||
selected: string | undefined;
|
||||
setSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
export function NodeTableContainer(props: NodeTableContainerProps) {
|
||||
const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
|
||||
const [order, setOrder] = React.useState<NodeOrder>("asc");
|
||||
const [orderBy, setOrderBy] = React.useState<keyof Machine>("status");
|
||||
|
||||
// Avoid a layout jump when reaching the last page with empty rows.
|
||||
const emptyRows =
|
||||
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0;
|
||||
|
||||
const handleRequestSort = (
|
||||
event: React.MouseEvent<unknown>,
|
||||
property: keyof Machine,
|
||||
) => {
|
||||
const isAsc = orderBy === property && order === "asc";
|
||||
setOrder(isAsc ? "desc" : "asc");
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const visibleRows = React.useMemo(
|
||||
() =>
|
||||
stableSort(tableData, getComparator(order, orderBy)).slice(
|
||||
page * rowsPerPage,
|
||||
page * rowsPerPage + rowsPerPage,
|
||||
),
|
||||
[order, orderBy, page, rowsPerPage, tableData],
|
||||
);
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table aria-labelledby="tableTitle" size={dense ? "small" : "medium"}>
|
||||
<EnhancedTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={tableData.length}
|
||||
/>
|
||||
<TableBody>
|
||||
{visibleRows.map((row, index) => {
|
||||
return (
|
||||
<NodeRow
|
||||
key={index}
|
||||
row={row}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow
|
||||
style={{
|
||||
height: (dense ? 33 : 53) * emptyRows,
|
||||
}}
|
||||
>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Card, CardContent, Stack, Typography } from "@mui/material";
|
||||
import hexRgb from "hex-rgb";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface PieData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface PieCardsProps {
|
||||
pieData: PieData[];
|
||||
}
|
||||
|
||||
export function PieCards(props: PieCardsProps) {
|
||||
const { pieData } = props;
|
||||
|
||||
const cardData = useMemo(() => {
|
||||
return pieData
|
||||
.filter((pieItem) => pieItem.value > 0)
|
||||
.concat({
|
||||
name: "Total",
|
||||
value: pieData.reduce((a, b) => a + b.value, 0),
|
||||
color: "#000000",
|
||||
});
|
||||
}, [pieData]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{ paddingTop: 6 }}
|
||||
height={350}
|
||||
id="cardBox"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="flex-start"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{cardData.map((pieItem) => (
|
||||
<Card
|
||||
key={pieItem.name}
|
||||
sx={{
|
||||
marginBottom: 2,
|
||||
marginRight: 2,
|
||||
width: 110,
|
||||
height: 110,
|
||||
backgroundColor: hexRgb(pieItem.color, {
|
||||
format: "css",
|
||||
alpha: 0.25,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="div"
|
||||
gutterBottom={true}
|
||||
textAlign="center"
|
||||
>
|
||||
{pieItem.value}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{ mb: 1.5 }}
|
||||
color="text.secondary"
|
||||
textAlign="center"
|
||||
>
|
||||
{pieItem.name}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { SetStateAction, Dispatch, useState, useEffect, useMemo } from "react";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { useDebounce } from "../hooks/useDebounce";
|
||||
import { Autocomplete, InputAdornment, TextField } from "@mui/material";
|
||||
import { Machine } from "@/api/model/machine";
|
||||
|
||||
export interface SearchBarProps {
|
||||
tableData: readonly Machine[];
|
||||
setFilteredList: Dispatch<SetStateAction<readonly Machine[]>>;
|
||||
}
|
||||
|
||||
export function SearchBar(props: SearchBarProps) {
|
||||
let { tableData, setFilteredList } = props;
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const debouncedSearch = useDebounce(search, 250);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Define a function to handle the Esc key press
|
||||
function handleEsc(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (event.key === "Escape") {
|
||||
setSearch("");
|
||||
setFilteredList(tableData);
|
||||
}
|
||||
|
||||
// check if the key is Enter
|
||||
if (event.key === "Enter") {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch) {
|
||||
const filtered: Machine[] = tableData.filter((row) => {
|
||||
return row.name.toLowerCase().includes(debouncedSearch.toLowerCase());
|
||||
});
|
||||
setFilteredList(filtered);
|
||||
}
|
||||
}, [debouncedSearch, tableData, setFilteredList]);
|
||||
|
||||
const handleInputChange = (event: any, value: string) => {
|
||||
if (value === "") {
|
||||
setFilteredList(tableData);
|
||||
}
|
||||
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const suggestions = useMemo(
|
||||
() => tableData.map((row) => row.name),
|
||||
[tableData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
autoComplete
|
||||
options={suggestions}
|
||||
renderOption={(props: any, option: any) => {
|
||||
return (
|
||||
<li {...props} key={option}>
|
||||
{option}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onKeyDown={handleEsc}
|
||||
onInputChange={handleInputChange}
|
||||
value={search}
|
||||
open={open}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
label="Search"
|
||||
variant="outlined"
|
||||
autoComplete="nickname"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
></TextField>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import Link from "next/link";
|
||||
|
||||
export function StickySpeedDial(props: { selected: string | undefined }) {
|
||||
const { selected } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
function handleClose(event: any, reason: CloseReason) {
|
||||
if (reason === "toggle" || reason === "escapeKeyDown") {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpen(event: any, reason: OpenReason) {
|
||||
if (reason === "toggle") {
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
const isSomethingSelected = selected != undefined;
|
||||
|
||||
function editDial() {
|
||||
if (isSomethingSelected) {
|
||||
return (
|
||||
<Link href={`/machines/edit/${selected}`} style={{ marginTop: 7.5 }}>
|
||||
<EditIcon color="action" />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <EditIcon color="disabled" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
transform: "translateZ(0px)",
|
||||
flexGrow: 1,
|
||||
position: "fixed",
|
||||
right: 20,
|
||||
top: 15,
|
||||
margin: 0,
|
||||
zIndex: 9000,
|
||||
}}
|
||||
>
|
||||
<SpeedDial
|
||||
color="secondary"
|
||||
ariaLabel="SpeedDial basic example"
|
||||
icon={<SpeedDialIcon />}
|
||||
direction="down"
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
open={open}
|
||||
>
|
||||
<SpeedDialAction
|
||||
key="Add"
|
||||
icon={
|
||||
<Link href="/machines/add" style={{ marginTop: 7.5 }}>
|
||||
<AddIcon color="action" />
|
||||
</Link>
|
||||
}
|
||||
tooltipTitle="Add"
|
||||
/>
|
||||
|
||||
<SpeedDialAction
|
||||
key="Delete"
|
||||
icon={
|
||||
<DeleteIcon color={isSomethingSelected ? "action" : "disabled"} />
|
||||
}
|
||||
tooltipTitle="Delete"
|
||||
/>
|
||||
<SpeedDialAction key="Edit" icon={editDial()} tooltipTitle="Edit" />
|
||||
</SpeedDial>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
export const schema: RJSFSchema = {
|
||||
properties: {
|
||||
bloatware: {
|
||||
properties: {
|
||||
age: {
|
||||
default: 42,
|
||||
description: "The age of the user",
|
||||
type: "integer",
|
||||
},
|
||||
isAdmin: {
|
||||
default: false,
|
||||
description: "Is the user an admin?",
|
||||
type: "boolean",
|
||||
},
|
||||
kernelModules: {
|
||||
default: ["nvme", "xhci_pci", "ahci"],
|
||||
description: "A list of enabled kernel modules",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
name: {
|
||||
default: "John Doe",
|
||||
description: "The name of the user",
|
||||
type: "string",
|
||||
},
|
||||
services: {
|
||||
properties: {
|
||||
opt: {
|
||||
default: "foo",
|
||||
description: "A submodule option",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
userIds: {
|
||||
additionalProperties: {
|
||||
type: "integer",
|
||||
},
|
||||
default: {
|
||||
albrecht: 3,
|
||||
horst: 1,
|
||||
peter: 2,
|
||||
},
|
||||
description: "Some attributes",
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
networking: {
|
||||
properties: {
|
||||
zerotier: {
|
||||
properties: {
|
||||
controller: {
|
||||
properties: {
|
||||
enable: {
|
||||
default: false,
|
||||
description:
|
||||
"Whether to enable turn this machine into the networkcontroller.",
|
||||
type: "boolean",
|
||||
},
|
||||
public: {
|
||||
default: false,
|
||||
description:
|
||||
"everyone can join a public network without having the administrator to accept\n",
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
networkId: {
|
||||
description: "zerotier networking id\n",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["networkId"],
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
export const schema: RJSFSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
default: "John-nixi",
|
||||
description: "The name of the machine",
|
||||
},
|
||||
age: {
|
||||
type: "integer",
|
||||
default: 42,
|
||||
description: "The age of the user",
|
||||
maximum: 40,
|
||||
},
|
||||
role: {
|
||||
enum: ["New York", "Amsterdam", "Hong Kong"],
|
||||
description: "Role of the user",
|
||||
},
|
||||
kernelModules: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
default: ["nvme", "xhci_pci", "ahci"],
|
||||
description: "A list of enabled kernel modules",
|
||||
},
|
||||
userIds: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
user: {
|
||||
type: "string",
|
||||
},
|
||||
id: {
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
default: [
|
||||
{
|
||||
user: "John",
|
||||
id: 12,
|
||||
},
|
||||
],
|
||||
description: "Some attributes",
|
||||
},
|
||||
xdg: {
|
||||
type: "object",
|
||||
properties: {
|
||||
portal: {
|
||||
type: "object",
|
||||
properties: {
|
||||
xdgOpenUsePortal: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
enable: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
lxqt: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enable: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
styles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
extraPortals: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
wlr: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enable: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
settings: {
|
||||
type: "object",
|
||||
default: {
|
||||
screencast: {
|
||||
output_name: "HDMI-A-1",
|
||||
max_fps: 30,
|
||||
exec_before: "disable_notifications.sh",
|
||||
exec_after: "enable_notifications.sh",
|
||||
chooser_type: "simple",
|
||||
chooser_cmd: "${pkgs.slurp}/bin/slurp -f %o -or",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,170 +0,0 @@
|
||||
export const status = {
|
||||
online: "online",
|
||||
offline: "offline",
|
||||
pending: "pending",
|
||||
} as const;
|
||||
// Convert object keys in a union type
|
||||
export type Status = (typeof status)[keyof typeof status];
|
||||
|
||||
export type Network = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ClanDevice = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: Status;
|
||||
ipv6: string;
|
||||
networks: Network[];
|
||||
};
|
||||
|
||||
export type ClanStatus = {
|
||||
self: ClanDevice;
|
||||
other: ClanDevice[];
|
||||
};
|
||||
export const clanStatus: ClanStatus = {
|
||||
self: {
|
||||
id: "1",
|
||||
name: "My Computer",
|
||||
ipv6: "",
|
||||
status: "online",
|
||||
networks: [
|
||||
{
|
||||
name: "Family",
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
name: "Fight-Club",
|
||||
id: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
// other: [],
|
||||
other: [
|
||||
{
|
||||
id: "2",
|
||||
name: "Daddies Computer",
|
||||
status: "online",
|
||||
networks: [
|
||||
{
|
||||
name: "Family",
|
||||
id: "1",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Lars Notebook",
|
||||
status: "offline",
|
||||
networks: [
|
||||
{
|
||||
name: "Family",
|
||||
id: "1",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Cassie Computer",
|
||||
status: "pending",
|
||||
networks: [
|
||||
{
|
||||
name: "Family",
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
name: "Fight-Club",
|
||||
id: "2",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Chuck Norris Computer",
|
||||
status: "online",
|
||||
networks: [
|
||||
{
|
||||
name: "Fight-Club",
|
||||
id: "2",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Ella Bright",
|
||||
status: "pending",
|
||||
networks: [
|
||||
{
|
||||
name: "Fight-Club",
|
||||
id: "2",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "Ryan Flabberghast",
|
||||
status: "offline",
|
||||
networks: [
|
||||
{
|
||||
name: "Fight-Club",
|
||||
id: "2",
|
||||
},
|
||||
],
|
||||
ipv6: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const severity = {
|
||||
info: "info",
|
||||
success: "success",
|
||||
warning: "warning",
|
||||
error: "error",
|
||||
} as const;
|
||||
// Convert object keys in a union type
|
||||
export type Severity = (typeof severity)[keyof typeof severity];
|
||||
|
||||
export type Notification = {
|
||||
id: string;
|
||||
msg: string;
|
||||
source: string;
|
||||
date: string;
|
||||
severity: Severity;
|
||||
};
|
||||
|
||||
export const notificationData: Notification[] = [
|
||||
{
|
||||
id: "1",
|
||||
date: "2022-12-27 08:26:49.219717",
|
||||
severity: "success",
|
||||
msg: "Defeated zombie mob flawless",
|
||||
source: "Chuck Norris Computer",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
date: "2022-12-27 08:26:49.219717",
|
||||
severity: "error",
|
||||
msg: "Application Crashed: my little pony",
|
||||
source: "Cassie Computer",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
date: "2022-12-27 08:26:49.219717",
|
||||
severity: "warning",
|
||||
msg: "Security update necessary",
|
||||
source: "Daddies Computer",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
date: "2022-12-27 08:26:49.219717",
|
||||
severity: "info",
|
||||
msg: "Decompressed snowflakes",
|
||||
source: "My Computer",
|
||||
},
|
||||
];
|
||||
@@ -1,178 +0,0 @@
|
||||
export interface TableData {
|
||||
name: string;
|
||||
status: NodeStatusKeys;
|
||||
last_seen: number;
|
||||
}
|
||||
|
||||
export const NodeStatus = {
|
||||
Online: "Online",
|
||||
Offline: "Offline",
|
||||
Pending: "Pending",
|
||||
};
|
||||
|
||||
export type NodeStatusKeys = (typeof NodeStatus)[keyof typeof NodeStatus];
|
||||
|
||||
function createData(
|
||||
name: string,
|
||||
status: NodeStatusKeys,
|
||||
last_seen: number,
|
||||
): TableData {
|
||||
if (status == NodeStatus.Online) {
|
||||
last_seen = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
status,
|
||||
last_seen: last_seen,
|
||||
};
|
||||
}
|
||||
|
||||
var nameNumber = 0;
|
||||
|
||||
// A function to generate random names
|
||||
function getRandomName(): string {
|
||||
let names = [
|
||||
"Alice",
|
||||
"Bob",
|
||||
"Charlie",
|
||||
"David",
|
||||
"Eve",
|
||||
"Frank",
|
||||
"Grace",
|
||||
"Heidi",
|
||||
"Ivan",
|
||||
"Judy",
|
||||
"Mallory",
|
||||
"Oscar",
|
||||
"Peggy",
|
||||
"Sybil",
|
||||
"Trent",
|
||||
"Victor",
|
||||
"Walter",
|
||||
"Wendy",
|
||||
"Zoe",
|
||||
];
|
||||
let index = Math.floor(Math.random() * names.length);
|
||||
return names[index] + nameNumber++;
|
||||
}
|
||||
|
||||
// A function to generate random IPv6 addresses
|
||||
// function getRandomId(): string {
|
||||
// let hex = "0123456789abcdef";
|
||||
// let id = "";
|
||||
// for (let i = 0; i < 8; i++) {
|
||||
// for (let j = 0; j < 4; j++) {
|
||||
// let index = Math.floor(Math.random() * hex.length);
|
||||
// id += hex[index];
|
||||
// }
|
||||
// if (i < 7) {
|
||||
// id += ":";
|
||||
// }
|
||||
// }
|
||||
// return id;
|
||||
// }
|
||||
|
||||
// A function to generate random status keys
|
||||
function getRandomStatus(): NodeStatusKeys {
|
||||
let statusKeys = [NodeStatus.Online, NodeStatus.Offline, NodeStatus.Pending];
|
||||
let index = Math.floor(Math.random() * statusKeys.length);
|
||||
return statusKeys[index];
|
||||
}
|
||||
|
||||
// A function to generate random last seen values
|
||||
function getRandomLastSeen(status: NodeStatusKeys): number {
|
||||
if (status === "online") {
|
||||
return 0;
|
||||
} else {
|
||||
let min = 1; // One day ago
|
||||
let max = 360; // One year ago
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
}
|
||||
|
||||
export const tableData = [
|
||||
createData(
|
||||
"Matchbox",
|
||||
|
||||
NodeStatus.Pending,
|
||||
0,
|
||||
),
|
||||
createData(
|
||||
"Ahorn",
|
||||
|
||||
NodeStatus.Online,
|
||||
0,
|
||||
),
|
||||
createData(
|
||||
"Yellow",
|
||||
|
||||
NodeStatus.Offline,
|
||||
16.0,
|
||||
),
|
||||
createData(
|
||||
"Rauter",
|
||||
|
||||
NodeStatus.Offline,
|
||||
6.0,
|
||||
),
|
||||
createData(
|
||||
"Porree",
|
||||
|
||||
NodeStatus.Offline,
|
||||
13,
|
||||
),
|
||||
createData(
|
||||
"Helsinki",
|
||||
|
||||
NodeStatus.Online,
|
||||
0,
|
||||
),
|
||||
createData(
|
||||
"Kelle",
|
||||
|
||||
NodeStatus.Online,
|
||||
0,
|
||||
),
|
||||
createData(
|
||||
"Shodan",
|
||||
|
||||
NodeStatus.Online,
|
||||
0.0,
|
||||
),
|
||||
createData(
|
||||
"Qubasa",
|
||||
|
||||
NodeStatus.Offline,
|
||||
7.0,
|
||||
),
|
||||
createData(
|
||||
"Green",
|
||||
|
||||
NodeStatus.Offline,
|
||||
2,
|
||||
),
|
||||
createData("Gum", NodeStatus.Offline, 0),
|
||||
createData("Xu", NodeStatus.Online, 0),
|
||||
createData(
|
||||
"Zaatar",
|
||||
|
||||
NodeStatus.Online,
|
||||
0,
|
||||
),
|
||||
];
|
||||
|
||||
// A function to execute the createData function with dummy data in a loop 100 times and return an array
|
||||
export function executeCreateData(): TableData[] {
|
||||
let result: TableData[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// Generate dummy data
|
||||
let name = getRandomName();
|
||||
let status = getRandomStatus();
|
||||
let last_seen = getRandomLastSeen(status);
|
||||
|
||||
// Call the createData function and push the result to the array
|
||||
result.push(createData(name, status, last_seen));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
export const tableData = [
|
||||
{
|
||||
name: "Wendy200",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 115,
|
||||
},
|
||||
{
|
||||
name: "Charlie201",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 320,
|
||||
},
|
||||
{
|
||||
name: "Bob202",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 347,
|
||||
},
|
||||
{
|
||||
name: "Sybil203",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Trent204",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Eve205",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Alice206",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 256,
|
||||
},
|
||||
{
|
||||
name: "Frank207",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 248,
|
||||
},
|
||||
{
|
||||
name: "Eve208",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 234,
|
||||
},
|
||||
{
|
||||
name: "Bob209",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 178,
|
||||
},
|
||||
{
|
||||
name: "Peggy210",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 256,
|
||||
},
|
||||
{
|
||||
name: "Heidi211",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Charlie212",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 15,
|
||||
},
|
||||
{
|
||||
name: "Victor213",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 171,
|
||||
},
|
||||
{
|
||||
name: "Heidi214",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 287,
|
||||
},
|
||||
{
|
||||
name: "Walter215",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Alice216",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Zoe217",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Judy218",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 184,
|
||||
},
|
||||
{
|
||||
name: "Mallory219",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Judy220",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 63,
|
||||
},
|
||||
{
|
||||
name: "Wendy221",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 181,
|
||||
},
|
||||
{
|
||||
name: "Bob222",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 300,
|
||||
},
|
||||
{
|
||||
name: "Eve223",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 60,
|
||||
},
|
||||
{
|
||||
name: "Judy224",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 218,
|
||||
},
|
||||
{
|
||||
name: "Peggy225",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 140,
|
||||
},
|
||||
{
|
||||
name: "Frank226",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 106,
|
||||
},
|
||||
{
|
||||
name: "Ivan227",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 296,
|
||||
},
|
||||
{
|
||||
name: "Charlie228",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 81,
|
||||
},
|
||||
{
|
||||
name: "Victor229",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 46,
|
||||
},
|
||||
{
|
||||
name: "Mallory230",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Wendy231",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 205,
|
||||
},
|
||||
{
|
||||
name: "David232",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 11,
|
||||
},
|
||||
{
|
||||
name: "Eve233",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 346,
|
||||
},
|
||||
{
|
||||
name: "David234",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Ivan235",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 291,
|
||||
},
|
||||
{
|
||||
name: "Mallory236",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 321,
|
||||
},
|
||||
{
|
||||
name: "David237",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Victor238",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Judy239",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 3,
|
||||
},
|
||||
{
|
||||
name: "Trent240",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Ivan241",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 38,
|
||||
},
|
||||
{
|
||||
name: "Mallory242",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Charlie243",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 288,
|
||||
},
|
||||
{
|
||||
name: "Ivan244",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 225,
|
||||
},
|
||||
{
|
||||
name: "Zoe245",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 56,
|
||||
},
|
||||
{
|
||||
name: "Trent246",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 111,
|
||||
},
|
||||
{
|
||||
name: "Sybil247",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Wendy248",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 299,
|
||||
},
|
||||
{
|
||||
name: "David249",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 303,
|
||||
},
|
||||
{
|
||||
name: "Trent250",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 69,
|
||||
},
|
||||
{
|
||||
name: "Eve251",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 354,
|
||||
},
|
||||
{
|
||||
name: "Oscar252",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 54,
|
||||
},
|
||||
{
|
||||
name: "Sybil253",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Zoe254",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 118,
|
||||
},
|
||||
{
|
||||
name: "Bob255",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 112,
|
||||
},
|
||||
{
|
||||
name: "Alice256",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Eve257",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 97,
|
||||
},
|
||||
{
|
||||
name: "Peggy258",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Ivan259",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 99,
|
||||
},
|
||||
{
|
||||
name: "Victor260",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 231,
|
||||
},
|
||||
{
|
||||
name: "Grace261",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 199,
|
||||
},
|
||||
{
|
||||
name: "Heidi262",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Sybil263",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 89,
|
||||
},
|
||||
{
|
||||
name: "Alice264",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 354,
|
||||
},
|
||||
{
|
||||
name: "Zoe265",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 12,
|
||||
},
|
||||
{
|
||||
name: "Victor266",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 24,
|
||||
},
|
||||
{
|
||||
name: "Ivan267",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 238,
|
||||
},
|
||||
{
|
||||
name: "Peggy268",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 113,
|
||||
},
|
||||
{
|
||||
name: "Oscar269",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Alice270",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Eve271",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Mallory272",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 180,
|
||||
},
|
||||
{
|
||||
name: "David273",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Eve274",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 164,
|
||||
},
|
||||
{
|
||||
name: "Walter275",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Eve276",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 123,
|
||||
},
|
||||
{
|
||||
name: "Wendy277",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 211,
|
||||
},
|
||||
{
|
||||
name: "Charlie278",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 178,
|
||||
},
|
||||
{
|
||||
name: "Eve279",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Zoe280",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Mallory281",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 143,
|
||||
},
|
||||
{
|
||||
name: "Bob282",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Judy283",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Grace284",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Zoe285",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Grace286",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 92,
|
||||
},
|
||||
{
|
||||
name: "Walter287",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Walter288",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 248,
|
||||
},
|
||||
{
|
||||
name: "David289",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 301,
|
||||
},
|
||||
{
|
||||
name: "Peggy290",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Sybil291",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 114,
|
||||
},
|
||||
{
|
||||
name: "Heidi292",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "David293",
|
||||
|
||||
status: "Offline",
|
||||
last_seen: 165,
|
||||
},
|
||||
{
|
||||
name: "Judy294",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 52,
|
||||
},
|
||||
{
|
||||
name: "Trent295",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Heidi296",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 129,
|
||||
},
|
||||
{
|
||||
name: "Trent297",
|
||||
|
||||
status: "Pending",
|
||||
last_seen: 108,
|
||||
},
|
||||
{
|
||||
name: "David298",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
{
|
||||
name: "Sybil299",
|
||||
|
||||
status: "Online",
|
||||
last_seen: 0,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user