Files
mengyamonitor/mengyamonitor-frontend/src/App.tsx

260 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import type { ServerConfig } from './types';
import { loadServers, saveServers, removeServer, exportServersToClipboard, importServersFromClipboard } from './utils/storage';
import { useServerMonitor } from './hooks/useServerMonitor';
import { ServerCard } from './components/ServerCard/ServerCard';
import { ServerDetail } from './components/ServerDetail/ServerDetail';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import type { DragEndEvent } from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
rectSortingStrategy,
} from '@dnd-kit/sortable';
import './App.css';
const formatServerUrl = (url: string) => {
let formatted = url.trim();
// Fix common typo .op -> .top based on user requirement
if (formatted.endsWith('.op')) {
formatted = formatted.slice(0, -3) + '.top';
}
if (formatted && !/^https?:\/\//i.test(formatted)) {
return `http://${formatted}`;
}
return formatted;
};
function App() {
const [servers, setServers] = useState<ServerConfig[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
const [selectedServerId, setSelectedServerId] = useState<string | null>(null);
const [newServerForm, setNewServerForm] = useState({ name: '', url: '' });
const [showHeader, setShowHeader] = useState(true);
const statuses = useServerMonitor(servers, 2000);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
useEffect(() => {
const loaded = loadServers();
setServers(loaded);
}, []);
const handleUrlBlur = () => {
const formatted = formatServerUrl(newServerForm.url);
if (formatted !== newServerForm.url) {
setNewServerForm(prev => ({ ...prev, url: formatted }));
}
};
const handleAddServer = () => {
if (!newServerForm.name || !newServerForm.url) {
alert('请填写服务器名称和地址');
return;
}
const formattedUrl = formatServerUrl(newServerForm.url);
const newServer: ServerConfig = {
id: Date.now().toString(),
name: newServerForm.name,
url: formattedUrl,
enabled: true,
};
const updated = [...servers, newServer];
setServers(updated);
saveServers(updated);
setNewServerForm({ name: '', url: '' });
setShowAddForm(false);
};
const handleRemoveServer = (serverId: string) => {
if (confirm('确定要移除这个服务器吗?')) {
const updated = servers.filter(s => s.id !== serverId);
setServers(updated);
removeServer(serverId);
}
};
const handleShowDetail = (serverId: string) => {
setSelectedServerId(serverId);
};
const handleExportServers = async () => {
try {
await exportServersToClipboard();
alert('服务器配置已复制到剪贴板!');
} catch (error) {
alert(error instanceof Error ? error.message : '导出失败');
}
};
const handleImportServers = async () => {
if (!confirm('导入服务器配置将添加到现有服务器列表中,是否继续?')) {
return;
}
try {
const importedServers = await importServersFromClipboard();
const updated = [...servers, ...importedServers];
setServers(updated);
saveServers(updated);
alert(`成功导入 ${importedServers.length} 个服务器配置!`);
} catch (error) {
alert(error instanceof Error ? error.message : '导入失败');
}
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setServers((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
const newOrder = arrayMove(items, oldIndex, newIndex);
saveServers(newOrder);
return newOrder;
});
}
};
const selectedStatus = selectedServerId ? statuses[selectedServerId] : null;
const selectedServer = servers.find(s => s.id === selectedServerId);
return (
<div className="app">
{!showHeader && (
<button
className="btn-show-header"
onClick={() => setShowHeader(true)}
title="显示导航栏"
>
👁
</button>
)}
{showHeader && (
<header className="app-header">
<h1>
<img className="app-logo" src="/logo.svg" alt="萌芽监控面板" />
</h1>
<div className="header-actions">
<button
className="btn-icon"
onClick={() => setShowHeader(false)}
title="隐藏导航栏"
>
👁
</button>
<button
className="btn-icon"
onClick={handleExportServers}
title="导出服务器配置到剪贴板"
>
📤
</button>
<button
className="btn-icon"
onClick={handleImportServers}
title="从剪贴板导入服务器配置"
>
📥
</button>
<button className="btn-add" onClick={() => setShowAddForm(!showAddForm)} title={showAddForm ? '取消' : '添加服务器'}>
{showAddForm ? '×' : '+'}
</button>
</div>
</header>
)}
{showAddForm && (
<div className="add-form">
<input
type="text"
placeholder="服务器名称"
value={newServerForm.name}
onChange={(e) => setNewServerForm({ ...newServerForm, name: e.target.value })}
/>
<input
type="text"
placeholder="服务器地址 (例如: http://192.168.1.100:9292)"
value={newServerForm.url}
onChange={(e) => setNewServerForm({ ...newServerForm, url: e.target.value })}
onBlur={handleUrlBlur}
/>
<button className="btn-submit" onClick={handleAddServer}>
</button>
</div>
)}
<main className="server-grid">
{servers.length === 0 ? (
<div className="empty-state">
<p></p>
<p className="hint">"添加服务器"使</p>
</div>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={servers.map((s) => s.id)} strategy={rectSortingStrategy}>
{servers.map((server) => {
const status = statuses[server.id];
// Calculate storage usage (max of all mounts)
const storageUsage = status?.metrics?.storage?.reduce((max, s) => Math.max(max, s.usedPercent), 0) || 0;
return (
<ServerCard
key={server.id}
server={server}
online={status?.online || false}
metrics={status?.metrics}
cpuUsage={status?.metrics?.cpu.usagePercent || 0}
memoryUsage={status?.metrics?.memory.usedPercent || 0}
storageUsage={storageUsage} // Pass max storage usage
uptime={status?.metrics?.uptimeSeconds}
onDetail={handleShowDetail}
onRemove={handleRemoveServer}
/>
);
})}
</SortableContext>
</DndContext>
)}
</main>
{selectedStatus?.metrics && selectedServer && (
<ServerDetail
metrics={selectedStatus.metrics}
serverName={selectedServer.name}
onClose={() => setSelectedServerId(null)}
/>
)}
</div>
);
}
export default App;