Add system info
This commit is contained in:
		@@ -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)]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 { 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>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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