Befor fixing linting problem

This commit is contained in:
2023-10-22 21:03:06 +02:00
parent 545d389df0
commit c7c47b6527
87 changed files with 703 additions and 3929 deletions

View File

@@ -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/*"]
}

View File

@@ -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

View File

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

View File

@@ -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>

View File

@@ -1,7 +0,0 @@
"use client";
import { CreateMachineForm } from "@/components/createMachineForm";
export default function CreateMachine() {
return <CreateMachineForm />;
}

View File

@@ -1,10 +0,0 @@
interface DeviceEditProps {
params: { name: string };
}
export default function EditDevice(props: DeviceEditProps) {
const {
params: { name },
} = props;
return <div>{name}</div>;
}

View File

@@ -1,5 +0,0 @@
import { MachineContextProvider } from "@/components/hooks/useMachines";
export default function Layout({ children }: { children: React.ReactNode }) {
return <MachineContextProvider>{children}</MachineContextProvider>;
}

View File

@@ -1,12 +0,0 @@
"use client";
import { NodeTable } from "@/components/table";
import { StrictMode } from "react";
export default function Page() {
return (
<StrictMode>
<NodeTable />
</StrictMode>
);
}

View File

@@ -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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
);

View File

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

View File

@@ -1 +0,0 @@
export { NodeTable } from "./nodeTable";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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,
},
];