Add system info

This commit is contained in:
Pierre HUBERT 2024-05-04 09:11:30 +02:00
parent 768ba03807
commit c7306bdd55
10 changed files with 261 additions and 2 deletions

View File

@ -84,12 +84,21 @@ pub struct VMState {
pub state: String, pub state: String,
} }
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct LoadAverage {
one: f64,
five: f64,
fifteen: f64,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct SystemSystemInfo { pub struct SystemSystemInfo {
physical_core_count: usize, physical_core_count: usize,
uptime: usize, uptime: usize,
used_memory: usize, used_memory: usize,
available_memory: usize, available_memory: usize,
free_memory: usize,
load_average: LoadAverage,
} }
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.49.0", "@fluentui/react-components": "^9.49.0",
"@fluentui/react-icons": "^2.0.238", "@fluentui/react-icons": "^2.0.238",
"filesize": "^10.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
@ -3856,6 +3857,14 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/filesize": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.1.tgz",
"integrity": "sha512-L0cdwZrKlwZQkMSFnCflJ6J2Y+5egO/p3vgRSDQGxQt++QbUZe5gMbRO6kg6gzwQDPvq2Fk9AmoxUNfZ5gdqaQ==",
"engines": {
"node": ">= 10.4.0"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",

View File

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.49.0", "@fluentui/react-components": "^9.49.0",
"@fluentui/react-icons": "^2.0.238", "@fluentui/react-icons": "^2.0.238",
"filesize": "^10.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },

View File

@ -3,6 +3,7 @@ import { ServerApi } from "./api/ServerApi";
import { AuthRouteWidget } from "./routes/AuthRouteWidget"; import { AuthRouteWidget } from "./routes/AuthRouteWidget";
import { AsyncWidget } from "./widgets/AsyncWidget"; import { AsyncWidget } from "./widgets/AsyncWidget";
import { MainMenu } from "./widgets/MainMenu"; import { MainMenu } from "./widgets/MainMenu";
import { SystemInfoWidget } from "./widgets/SystemInfoWidget";
const useStyles = makeStyles({ const useStyles = makeStyles({
title: typographyStyles.title2, title: typographyStyles.title2,
@ -29,7 +30,7 @@ function AppInner(): React.ReactElement {
return ( return (
<div <div
style={{ style={{
width: "100%", width: "95%",
maxWidth: "1000px", maxWidth: "1000px",
margin: "50px auto", margin: "50px auto",
}} }}
@ -38,6 +39,7 @@ function AppInner(): React.ReactElement {
<span className={styles.title}>VirtWebRemote</span> <span className={styles.title}>VirtWebRemote</span>
<MainMenu /> <MainMenu />
</div> </div>
<SystemInfoWidget />
</div> </div>
); );
} }

View File

@ -0,0 +1,42 @@
import { APIClient } from "./ApiClient";
export interface SysInfoConfig {
allowed: boolean;
}
export interface LoadAverage {
one: number;
five: number;
fifteen: number;
}
export interface SysInfoStatusSystem {
physical_core_count: number;
uptime: number;
used_memory: number;
available_memory: number;
free_memory: number;
load_average: LoadAverage;
}
export interface SysInfoStatus {
system: SysInfoStatusSystem;
}
export class SysInfoApi {
/**
* Get system info configuration (ie. check if it allowed)
*/
static async GetConfig(): Promise<SysInfoConfig> {
return (await APIClient.exec({ method: "GET", uri: "/sysinfo/config" }))
.data;
}
/**
* Get system status
*/
static async Status(): Promise<SysInfoStatus> {
return (await APIClient.exec({ method: "GET", uri: "/sysinfo/status" }))
.data;
}
}

View File

@ -0,0 +1,44 @@
import {
Toast,
ToastBody,
ToastTitle,
Toaster,
useId,
useToastController,
} from "@fluentui/react-components";
import React, { PropsWithChildren } from "react";
type ToastContext = (
title: string,
body: string,
intent?: "error" | "info" | "success" | "warning"
) => void;
const ToastContextK = React.createContext<ToastContext | null>(null);
export function ToastProvider(p: PropsWithChildren): React.ReactElement {
const toasterId = useId("toaster");
const { dispatchToast } = useToastController(toasterId);
const hook: ToastContext = (title, body, intent) => {
dispatchToast(
<Toast>
<ToastTitle>{title}</ToastTitle>
<ToastBody>{body}</ToastBody>
</Toast>,
{ intent: intent ?? "info" }
);
};
return (
<>
<ToastContextK.Provider value={hook}>{p.children}</ToastContextK.Provider>
<Toaster toasterId={toasterId} />
</>
);
}
export function useToast(): ToastContext {
return React.useContext(ToastContextK)!;
}

View File

@ -5,13 +5,16 @@ import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider"; import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
import { ThemeProvider } from "./hooks/providers/ThemeProvider"; import { ThemeProvider } from "./hooks/providers/ThemeProvider";
import "./index.css"; import "./index.css";
import { ToastProvider } from "./hooks/providers/ToastProvider";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider> <ThemeProvider>
<AlertDialogProvider> <AlertDialogProvider>
<ConfirmDialogProvider> <ConfirmDialogProvider>
<App /> <ToastProvider>
<App />
</ToastProvider>
</ConfirmDialogProvider> </ConfirmDialogProvider>
</AlertDialogProvider> </AlertDialogProvider>
</ThemeProvider> </ThemeProvider>

View File

@ -0,0 +1,10 @@
export function format_duration(duration: number): string {
const secs = duration % 60;
const mins = ((duration - secs) / 60) % 60;
const hours = ((duration - secs - mins * 60) / 3600) % 3600;
const days = Math.floor(duration / (3600 * 24));
return `${days} days ${hours.toString().padStart(2, "0")}:${mins
.toString()
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
}

View File

@ -0,0 +1,19 @@
import { makeStyles, typographyStyles } from "@fluentui/react-components";
import React from "react";
const useStyles = makeStyles({
title: typographyStyles.title3,
});
export function SectionContainer(
p: React.PropsWithChildren<{ title: string }>
): React.ReactElement {
const styles = useStyles();
return (
<div style={{ margin: "100px 0px" }}>
<span className={styles.title}>{p.title}</span>
<div style={{ height: "20px" }}></div>
{p.children}
</div>
);
}

View File

@ -0,0 +1,120 @@
import React from "react";
import { SysInfoApi, SysInfoConfig, SysInfoStatus } from "../api/SysInfoApi";
import { AsyncWidget } from "./AsyncWidget";
import { SectionContainer } from "./SectionContainer";
import { Field, ProgressBar } from "@fluentui/react-components";
import { filesize } from "filesize";
import { format_duration } from "../utils/time_utils";
import { useToast } from "../hooks/providers/ToastProvider";
export function SystemInfoWidget(): React.ReactElement {
const [config, setConfig] = React.useState<SysInfoConfig | undefined>();
const load = async () => {
setConfig(await SysInfoApi.GetConfig());
};
return (
<SectionContainer title="System info">
<AsyncWidget
loadKey={1}
load={load}
errMsg="Failed to check system configuration!"
loadingMessage="Checking server configuration..."
build={() =>
config?.allowed ? (
<SystemInfoWidgetInner />
) : (
<SystemInfoWidgetUnavailable />
)
}
/>
</SectionContainer>
);
}
function SystemInfoWidgetUnavailable(): React.ReactElement {
return (
<p style={{ textAlign: "center" }}>
Unfortunatley, system information is available. (not enough privileges)
</p>
);
}
function SystemInfoWidgetInner(): React.ReactElement {
const toast = useToast();
const [status, setStatus] = React.useState<SysInfoStatus | undefined>();
const load = async () => {
setStatus(await SysInfoApi.Status());
};
React.useEffect(() => {
const interval = setInterval(async () => {
try {
await load();
} catch (e) {
console.error(e);
toast("Error", "Failed to refresh system status!", "error");
}
}, 1500);
return () => clearInterval(interval);
});
return (
<AsyncWidget
loadKey={1}
load={load}
loadingMessage="Loading system status..."
errMsg="Failed to load system status!"
build={() => (
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}
>
<Field
validationMessage={`${filesize(
status!.system.used_memory
)} of memory used out of ${filesize(
status!.system.available_memory + status!.system.used_memory
)}`}
validationState="none"
style={{ flex: 1 }}
>
<ProgressBar
value={
status!.system.used_memory /
(status!.system.available_memory + status!.system.used_memory)
}
/>
</Field>
<div style={{ flex: 1 }}>
<p>
Load average: {status!.system.load_average.one}{" "}
{status!.system.load_average.five}{" "}
{status!.system.load_average.fifteen}
</p>
<UptimeWidget uptime={status!.system.uptime} />
Number physical cores: {status!.system.physical_core_count}
</div>
</div>
)}
/>
);
}
function UptimeWidget(p: { uptime: number }): React.ReactElement {
const [uptime, setUptime] = React.useState(p.uptime);
React.useEffect(() => {
const interval = setInterval(() => setUptime((uptime) => uptime + 1), 1000);
return () => clearInterval(interval);
}, [p.uptime]);
return <p>Uptime: {format_duration(uptime)}</p>;
}