Skip to content

Commit

Permalink
feat: Add dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
NriotHrreion committed Aug 9, 2024
1 parent 4353a4e commit 5960ad2
Show file tree
Hide file tree
Showing 23 changed files with 718 additions and 28 deletions.
40 changes: 38 additions & 2 deletions app/(pages)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
/* eslint-disable padding-line-between-statements */
"use client";

import { useEffect } from "react";
import React, { useState, useEffect } from "react";

import { useDetectCookie } from "@/hooks/useDetectCookie";
// Widgets
import CPUWidget from "@/components/dashboard/cpu-widget";
import DiskWidget from "@/components/dashboard/disk-widget";
import MemoryWidget from "@/components/dashboard/memory-widget";
import GPUWidget from "@/components/dashboard/gpu-widget";
import BatteryWidget from "@/components/dashboard/battery-widget";
import OSWidget from "@/components/dashboard/os-widget";
import { WebSocketContext } from "@/hooks/useOS";

export default function Page() {
const [mounted, setMounted] = useState<boolean>(false);
const [ws, setWebSocket] = useState<WebSocket | null>(null);

useEffect(() => {
setMounted(true);

document.title = "Ferrum - 仪表盘";

const _ws = new WebSocket(`ws://${window.location.host}/api/os`);
setWebSocket(_ws);

return () => _ws?.close();
}, []);

useEffect(() => {
return () => ws?.close();
}, [ws]);

useDetectCookie();

return <></>;
if(!mounted) return <></>;

return (
<WebSocketContext.Provider value={{ ws }}>
<div className="w-[1000px] h-[600px] mx-auto b-15 p-5 grid grid-cols-4 grid-rows-3 gap-4">
<CPUWidget className="col-span-2"/>
<DiskWidget className="col-span-2 row-span-2"/>
<MemoryWidget className="col-span-2"/>
<GPUWidget className="col-span-2"/>
<BatteryWidget />
<OSWidget />
</div>
</WebSocketContext.Provider>
);
}
2 changes: 1 addition & 1 deletion app/(pages)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function Page() {
if(!settings) return <></>;

return (
<div className={cn("w-[800px] min-h-0 mx-auto mb-9 px-5 py-5 overflow-y-auto flex flex-col gap-10", scrollbarStyle)}>
<div className={cn("w-[800px] min-h-0 mx-auto mb-9 p-5 overflow-y-auto flex flex-col gap-10", scrollbarStyle)}>
<SettingsSection title="通用">
<SettingsItem label="Ace Editor 自动换行" description="文本编辑器自动换行">
<Switch
Expand Down
11 changes: 9 additions & 2 deletions app/api/fs/disks/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os from "node:os";

import { NextRequest } from "next/server";
import * as diskinfo from "node-disk-info";
import si from "systeminformation";

import { tokenStorageKey } from "@/lib/global";
import { validateToken } from "@/lib/token";
Expand All @@ -14,10 +14,17 @@ export async function GET(req: NextRequest) {
if(!validateToken(token)) return error(403);

try {
const disk = await si.fsSize();

return packet({
status: 200,
system: os.platform(),
disks: diskinfo.getDiskInfoSync()
disks: disk.map((item) => ({
used: item.used,
size: item.size,
capacity: (item.used / item.size) * 100,
mount: item.mount
}))
});
} catch (err) {
// eslint-disable-next-line no-console
Expand Down
105 changes: 105 additions & 0 deletions app/api/os/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable no-console */
import type { IncomingMessage } from "http";
import type { WebSocket, WebSocketServer } from "ws";
import type { OSWebSocketMessage } from "@/hooks/useOS";

import os from "os";

import si from "systeminformation";
import { cpuModel, usagePercent as cpuPercent } from "node-system-stats";
import cookie from "cookie";

import { error } from "@/lib/packet";
import { tokenStorageKey } from "@/lib/global";
import { validateToken } from "@/lib/token";

export function GET() {
return error(400);
}

export function SOCKET(
client: WebSocket,
req: IncomingMessage,
_server: WebSocketServer,
) {
const token = cookie.parse(req.headers.cookie ?? "")[tokenStorageKey];

if(!token) {
client.close(401);

return;
}
if(!validateToken(token)) {
client.close(403);

return;
}

console.log("[Server: /api/os] Socket client connected.");

const handleRequest = async () => {
const cpu = await si.cpu();
const cpuTemp = await si.cpuTemperature();
const mem = await si.mem();
const memLayout = await si.memLayout();
const graphics = await si.graphics();
const battery = await si.battery();
const disk = await si.fsSize();

// if(cpuTemp.main === null) {
// console.warn("CPU temperature info on Windows requires Administrator privilege.");
// }

client.send(JSON.stringify({
cpu: {
model: cpuModel,
totalCores: cpu.cores,
usage: (await cpuPercent()).percent,

/**
* Admin privilege required on Windows
* @see https://systeminformation.io/cpu.html
*/
temperature: cpuTemp.main
},
memory: {
total: mem.total,
usage: (mem.used / mem.total) * 100,
amount: memLayout.length
},
gpu: graphics.controllers.map((gpu) => ({
model: gpu.model,
vendor: gpu.vendor,
memoryTotal: gpu.vram,
memoryUsage: gpu.memoryUsed && gpu.memoryTotal ? ((gpu.memoryUsed / gpu.memoryTotal) * 100) : undefined
})),
battery: {
hasBattery: battery.hasBattery,
isCharging: battery.isCharging,
percent: battery.percent
},
disk: disk.map((item) => ({
mount: item.mount,
type: item.type,
size: item.size,
used: item.used
})),
os: {
arch: os.arch(),
platform: os.platform(),
release: os.release(),
version: os.version()
}
} as OSWebSocketMessage));
};

var timer = setInterval(handleRequest, 5000);

handleRequest();

client.on("close", () => {
clearInterval(timer);

console.log("[Server: /api/os] Socket client disconnected.");
});
}
1 change: 1 addition & 0 deletions app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import { ToastContainer } from "react-toastify";

// Dialogs
import RenameFolderDialog from "@/components/dialogs/rename-folder-dialog";
import RenameFileDialog from "@/components/dialogs/rename-file-dialog";
import RemoveFolderDialog from "@/components/dialogs/remove-folder-dialog";
Expand Down
45 changes: 45 additions & 0 deletions components/dashboard/battery-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { PropsWithCN } from "@/types";

import React, { useState } from "react";
import { Progress } from "@nextui-org/progress";

import DashboardWidget from "./dashboard-widget";

import { type BatteryInfo, useOS } from "@/hooks/useOS";

const BatteryWidget: React.FC<PropsWithCN> = (props) => {
const [batteryInfo, setBatteryInfo] = useState<BatteryInfo | null>(null);

useOS(({ battery }) => {
setBatteryInfo(battery);
});

return (
<DashboardWidget
name="电池状态"
className={props.className}
insideClassName="flex flex-col justify-between">
{
batteryInfo?.hasBattery
? (
<>
<p className="text-green-700 dark:text-green-500">{batteryInfo?.isCharging ? "正在充电" : ""}</p>

<div className="flex flex-col gap-4">
<span className="text-3xl font-semibold">{batteryInfo?.percent ? `${batteryInfo?.percent}%` : "--%"}</span>

<Progress
classNames={{ indicator: "bg-green-600 dark:bg-green-500" }}
size="sm"
value={batteryInfo?.percent}
aria-label="电池电量"/>
</div>
</>
)
: <p className="text-default-500">未发现电池</p>
}
</DashboardWidget>
);
}

export default BatteryWidget;
42 changes: 42 additions & 0 deletions components/dashboard/cpu-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { PropsWithCN } from "@/types";

import React, { useState } from "react";
import { Progress } from "@nextui-org/progress";

import DashboardWidget from "./dashboard-widget";

import { type CPUInfo, useOS } from "@/hooks/useOS";

const CPUWidget: React.FC<PropsWithCN> = (props) => {
const [cpuInfo, setCPUInfo] = useState<CPUInfo | null>(null);

useOS(({ cpu }) => {
setCPUInfo(cpu);
});

return (
<DashboardWidget
name="CPU 占用"
className={props.className}
insideClassName="flex flex-col justify-between">
<p className="text-default-500">{cpuInfo?.model} {cpuInfo?.totalCores ? `(${cpuInfo.totalCores}核)` : ""}</p>

<div className="flex flex-col gap-4">
<div className="flex justify-between items-end">
<span className="">
<span className="text-default-500">温度:</span>
{cpuInfo?.temperature ? `${cpuInfo?.temperature.toFixed(2)}°C` : "--°C"}
</span>
<span className="text-3xl font-semibold">{cpuInfo?.usage ? `${cpuInfo?.usage}%` : "--%"}</span>
</div>

<Progress
size="sm"
value={cpuInfo?.usage}
aria-label="CPU 占用"/>
</div>
</DashboardWidget>
);
}

export default CPUWidget;
24 changes: 24 additions & 0 deletions components/dashboard/dashboard-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { PropsWithCN } from "@/types";

import React, { type PropsWithChildren } from "react";
import { Card } from "@nextui-org/card";
import { cn } from "@nextui-org/theme";

interface DashboardWidgetProps extends PropsWithCN, PropsWithChildren {
name: string;
insideClassName?: string
}

const DashboardWidget: React.FC<DashboardWidgetProps> = (props) => {
return (
<Card className={cn("px-5 py-4 flex flex-col gap-2", props.className)}>
<span className="text-xl font-semibold">{props.name}</span>

<div className={cn("flex-1", props.insideClassName)}>
{props.children}
</div>
</Card>
);
}

export default DashboardWidget;
Loading

0 comments on commit 5960ad2

Please sign in to comment.