Add system info
This commit is contained in:
		@@ -84,12 +84,21 @@ pub struct VMState {
 | 
			
		||||
    pub state: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
 | 
			
		||||
pub struct LoadAverage {
 | 
			
		||||
    one: f64,
 | 
			
		||||
    five: f64,
 | 
			
		||||
    fifteen: f64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
 | 
			
		||||
pub struct SystemSystemInfo {
 | 
			
		||||
    physical_core_count: usize,
 | 
			
		||||
    uptime: usize,
 | 
			
		||||
    used_memory: usize,
 | 
			
		||||
    available_memory: usize,
 | 
			
		||||
    free_memory: usize,
 | 
			
		||||
    load_average: LoadAverage,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -10,6 +10,7 @@
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@fluentui/react-components": "^9.49.0",
 | 
			
		||||
        "@fluentui/react-icons": "^2.0.238",
 | 
			
		||||
        "filesize": "^10.1.1",
 | 
			
		||||
        "react": "^18.2.0",
 | 
			
		||||
        "react-dom": "^18.2.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -3856,6 +3857,14 @@
 | 
			
		||||
        "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": {
 | 
			
		||||
      "version": "7.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fluentui/react-components": "^9.49.0",
 | 
			
		||||
    "@fluentui/react-icons": "^2.0.238",
 | 
			
		||||
    "filesize": "^10.1.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { ServerApi } from "./api/ServerApi";
 | 
			
		||||
import { AuthRouteWidget } from "./routes/AuthRouteWidget";
 | 
			
		||||
import { AsyncWidget } from "./widgets/AsyncWidget";
 | 
			
		||||
import { MainMenu } from "./widgets/MainMenu";
 | 
			
		||||
import { SystemInfoWidget } from "./widgets/SystemInfoWidget";
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles({
 | 
			
		||||
  title: typographyStyles.title2,
 | 
			
		||||
@@ -29,7 +30,7 @@ function AppInner(): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        width: "100%",
 | 
			
		||||
        width: "95%",
 | 
			
		||||
        maxWidth: "1000px",
 | 
			
		||||
        margin: "50px auto",
 | 
			
		||||
      }}
 | 
			
		||||
@@ -38,6 +39,7 @@ function AppInner(): React.ReactElement {
 | 
			
		||||
        <span className={styles.title}>VirtWebRemote</span>
 | 
			
		||||
        <MainMenu />
 | 
			
		||||
      </div>
 | 
			
		||||
      <SystemInfoWidget />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								remote_frontend/src/api/SysInfoApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								remote_frontend/src/api/SysInfoApi.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								remote_frontend/src/hooks/providers/ToastProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								remote_frontend/src/hooks/providers/ToastProvider.tsx
									
									
									
									
									
										Normal 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)!;
 | 
			
		||||
}
 | 
			
		||||
@@ -5,13 +5,16 @@ import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
 | 
			
		||||
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
import { ThemeProvider } from "./hooks/providers/ThemeProvider";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
import { ToastProvider } from "./hooks/providers/ToastProvider";
 | 
			
		||||
 | 
			
		||||
ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <ThemeProvider>
 | 
			
		||||
      <AlertDialogProvider>
 | 
			
		||||
        <ConfirmDialogProvider>
 | 
			
		||||
          <App />
 | 
			
		||||
          <ToastProvider>
 | 
			
		||||
            <App />
 | 
			
		||||
          </ToastProvider>
 | 
			
		||||
        </ConfirmDialogProvider>
 | 
			
		||||
      </AlertDialogProvider>
 | 
			
		||||
    </ThemeProvider>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								remote_frontend/src/utils/time_utils.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								remote_frontend/src/utils/time_utils.tsx
									
									
									
									
									
										Normal 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")}`;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								remote_frontend/src/widgets/SectionContainer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								remote_frontend/src/widgets/SectionContainer.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								remote_frontend/src/widgets/SystemInfoWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								remote_frontend/src/widgets/SystemInfoWidget.tsx
									
									
									
									
									
										Normal 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>;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user