diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0e5f5fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Uninstantiated" + ] +} \ No newline at end of file diff --git a/web/src/assets/svg/server-outlined.svg b/web/src/assets/svg/server-outlined.svg new file mode 100644 index 0000000..ce67aad --- /dev/null +++ b/web/src/assets/svg/server-outlined.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/assets/svg/server.svg b/web/src/assets/svg/server.svg new file mode 100644 index 0000000..1b2e8c5 --- /dev/null +++ b/web/src/assets/svg/server.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/common/ErrorContent.tsx b/web/src/common/ErrorContent.tsx new file mode 100644 index 0000000..e04a996 --- /dev/null +++ b/web/src/common/ErrorContent.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Typography } from "@mui/material"; +import clsx from "clsx"; +import SentimentVeryDissatisfiedIcon from "@mui/icons-material/SentimentVeryDissatisfied"; + +interface ErrorContentType { + title: string; + description: string; + className?: string; + onTryAgain?: () => void; +} + +/** + * + * @param {ErrorContentProps} props + */ + +function ErrorContent(props: ErrorContentType): JSX.Element { + const { title, description, className, onTryAgain, ...rest } = props; + + return ( +
+ + {title} + +
+ +
+ + {description} + +
+ ); +} + +ErrorContent.defaultProps = { + title: "Something went wrong", + description: "We're quite sorry about this!", +}; + +export default ErrorContent; + +/** + * @typedef {{ + * onTryAgain: Function + * } & import("@mui/material").PaperProps} ErrorContentProps + */ diff --git a/web/src/common/ErrorDialog.tsx b/web/src/common/ErrorDialog.tsx new file mode 100644 index 0000000..7861588 --- /dev/null +++ b/web/src/common/ErrorDialog.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogContentText, + DialogTitle, + DialogActions, + Button, +} from "@mui/material"; + +interface ErrorDialogProps { + title?: string; + description?: string; + onRetry?: () => void; + retryText?: string; +} +/** + * + * @param {ErrorDialogProps} props + */ +export function ErrorDialog(props: ErrorDialogProps) { + const { title, description, onRetry, retryText, ...rest } = props; + function handleRetry(e: any) { + e.stopPropagation(); + if (onRetry) { + onRetry(); + } + } + + return ( + + {title} + + {description} + + + + + + ); +} + +ErrorDialog.defaultProps = { + title: "Something went wrong", + description: + "Sorry, something went wrong, Please try again later or contact our support.", + retryText: "Try Again", +}; + +export default ErrorDialog; + +/** + * @typedef {import("./ErrorDialogContext").ErrorOptions & import("@material-ui/core").DialogProps} ErrorDialogProps + */ diff --git a/web/src/common/LoadingContent.tsx b/web/src/common/LoadingContent.tsx new file mode 100644 index 0000000..bb1940f --- /dev/null +++ b/web/src/common/LoadingContent.tsx @@ -0,0 +1,98 @@ +import React, { FC, ReactNode, useEffect } from "react"; +import useDataRef from "hooks/useDataRef"; +import LoadingIndicator from "./LoadingIndicator"; +import ErrorContent from "common/ErrorContent"; +import { Box } from "@mui/material"; + +interface LoadingContentType { + size: number; + error: boolean; + loading: boolean; + children: JSX.Element | (() => JSX.Element); + onReload: () => void; + onMount: () => void; + loadingContent: JSX.Element | ((x: JSX.Element) => ReactNode); + errorContent: JSX.Element | ((x: JSX.Element) => ReactNode); + className: string; +} +/** + * + * @param {LoadingContentProps} props + */ +function LoadingContent(props: Partial): JSX.Element { + const { + size, + error, + loading, + children, + onReload, + onMount, + loadingContent, + errorContent, + className, + ...rest + } = props; + + const dataRef = useDataRef({ onReload, onMount }); + + useEffect(() => { + dataRef.current.onMount?.(); + }, [dataRef]); + + if (!loading && !error) { + if (children !== undefined) { + return typeof children === "function" ? children() : children; + } + } + + const defaultLoadingContent = ; + + const defaultErrorContent = onReload?.()} />; + + return ( + + {error ? ( + <> + {errorContent + ? typeof errorContent === "function" + ? errorContent(defaultErrorContent) + : errorContent + : defaultErrorContent} + + ) : loadingContent ? ( + typeof loadingContent === "function" ? ( + loadingContent(defaultLoadingContent) + ) : ( + loadingContent + ) + ) : ( + defaultLoadingContent + )} + + ); +} + +LoadingContent.defaultProps = { + size: 40, + children: null, +}; + +export default LoadingContent; + +/** + * @typedef {{ + * size: string | number, + * onMount: Function, + * onReload: Function, + * error: boolean, + * loading: boolean, + * errorContent: React.ReactNode, + * loadingContent: React.ReactNode, + * } & React.ComponentPropsWithoutRef<'div'>} LoadingContentProps + */ diff --git a/web/src/common/LoadingIndicator.tsx b/web/src/common/LoadingIndicator.tsx new file mode 100644 index 0000000..458c974 --- /dev/null +++ b/web/src/common/LoadingIndicator.tsx @@ -0,0 +1,12 @@ +import { CircularProgress } from "@mui/material"; +import React from "react"; + +/** + * + * @param {import("@mui/material").CircularProgressProps} props + */ +function LoadingIndicator(props: { [rest: string]: any }) { + return ; +} + +export default LoadingIndicator; diff --git a/web/src/hooks/useDataRef.tsx b/web/src/hooks/useDataRef.tsx new file mode 100644 index 0000000..092cd0e --- /dev/null +++ b/web/src/hooks/useDataRef.tsx @@ -0,0 +1,13 @@ +import { useRef } from "react"; + +/** + * @template T + * @param {T} data + */ +function useDataRef(data: any) { + const ref = useRef(data); + ref.current = data; + return ref; +} + +export default useDataRef; diff --git a/web/src/server/ServerList.tsx b/web/src/server/ServerList.tsx index 01e900f..8a85bcf 100644 --- a/web/src/server/ServerList.tsx +++ b/web/src/server/ServerList.tsx @@ -1,20 +1,33 @@ -import React from "react"; +import { Card, CardActionArea, Grid, Typography } from "@mui/material"; +import { Box, Container } from "@mui/system"; +import React, { useState } from "react"; import useWebSocket, { ReadyState } from "react-use-websocket"; +import { ReactComponent as ServerIcon } from "../assets/svg/server.svg"; +import LoadingContent from "../common/LoadingContent"; +import ThemeConfig from "../ThemeConfig"; +import { ServerResponse } from "./ServerType"; export default function ServerList() { + const [servers, setServers] = useState([]); const { sendJsonMessage, getWebSocket, readyState } = useWebSocket( `ws://localhost:3000/metrics`, { onOpen: () => console.log("WebSocket connection opened."), onClose: () => console.log("WebSocket connection closed."), shouldReconnect: (closeEvent) => true, - onMessage: (event: WebSocketEventMap["message"]) => - console.log("new Data", event.data), + onMessage: (event: WebSocketEventMap["message"]) => { + const newMessage: ServerResponse = JSON.parse(event.data); + setServers((prev: ServerResponse[]) => { + if (!newMessage.Error) { + return prev.concat(newMessage); + } + }); + }, } ); getWebSocket(); - const connectionStatus = { + const connectionStatus: string = { [ReadyState.CONNECTING]: "Connecting", [ReadyState.OPEN]: "Open", [ReadyState.CLOSING]: "Closing", @@ -22,6 +35,44 @@ export default function ServerList() { [ReadyState.UNINSTANTIATED]: "Uninstantiated", }[readyState]; + console.log(servers); // console.log("getWebsocket", getWebSocket()?.OPEN); - return
ServerList
; + return ( + + + + {servers.map((server: any, index: number) => ( + + + + + + + localhost -{" "} + + linux + + + + + + + ))} + + + + ); } diff --git a/web/src/server/ServerType.tsx b/web/src/server/ServerType.tsx new file mode 100644 index 0000000..d3fb706 --- /dev/null +++ b/web/src/server/ServerType.tsx @@ -0,0 +1,16 @@ +export interface ServerResponse { + Error: boolean; + Message: { + Host: string; + Name: + | "disk" + | "docker" + | "uptime" + | "memory" + | "process" + | "loadavg" + | "tcp"; + Platform: string; + Data: Object; + }; +} diff --git a/web/tsconfig.json b/web/tsconfig.json index a273b0c..9aa2001 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -6,6 +6,7 @@ "dom.iterable", "esnext" ], + "baseUrl": "src", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true,