4c1dc4082e
Build and Push Images / build-and-push (push) Has been cancelled
이전 세션들에서 작업된 아래 범위를 모두 포함: Fleet 서브시스템 (src/fleet/) - fleetDeviceService / fleetCommandService / fleetDeploymentService / fleetReleaseService - fleetMetricsService, fleetScriptService, fleetEdgeConfigService - Edge 디바이스 관리, 커맨드 발행, 배포/릴리스, 스크립트 동기화 Collector 확장 - centralMqttForwarder / centralForwarderConfigService - equipmentStateService, pythonHookRunner, scriptCache - Modbus/OPC-UA/S7/XGT 프로토콜 클라이언트 - targetDbIntrospection (저장 DB 조회) Routes / API - automationDashboardRoutes, centralForwarderRoutes, equipmentStateRoutes DB - importEdgeConfig (Python cached config → Pipeline DB) - seedDataSources (external_db_connections 초기 시드) 엣지 배포 리소스 - docker/edge/Dockerfile.backend.prod, Dockerfile.frontend.prod - docker/edge/docker-compose.edge.yml 프론트엔드 - admin/automaticMng (centralForwarder, dashboard, equipmentState) - admin/fleet (commands, devices, deployments, releases, scripts, alerts) - admin/pipeline-device 개선 (저장 DB 드롭다운, 태그 매핑 등) - ExternalDbConnectionModal, ScriptsManagerDialog 등 신규 컴포넌트 - lib/api: automationDashboard, centralForwarder, equipmentState, fleet docs/ - EDGE_SERVER_STRUCTURE, FLEET_COMPLETE, FLEET_EDGE_INTEGRATION, FLEET_HOOK_INTEGRATION Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.5 KiB
TypeScript
116 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import { fleetApi, FleetCommand } from "@/lib/api/fleet";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { RefreshCw, Terminal, Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
pending: "bg-gray-500/10 text-gray-600",
|
|
sent: "bg-blue-500/10 text-blue-600",
|
|
executing: "bg-amber-500/10 text-amber-600",
|
|
success: "bg-green-500/10 text-green-600",
|
|
failed: "bg-red-500/10 text-red-600",
|
|
timeout: "bg-orange-500/10 text-orange-600",
|
|
};
|
|
|
|
export default function FleetCommandsPage() {
|
|
const [commands, setCommands] = useState<FleetCommand[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const r = await fleetApi.getCommands({ limit: 100 });
|
|
setCommands(r.data || []);
|
|
} catch { toast.error("커맨드 이력 조회 실패"); }
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
const t = setInterval(load, 10000);
|
|
return () => clearInterval(t);
|
|
}, [load]);
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-background">
|
|
<div className="shrink-0 p-6 pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-bold">Fleet 커맨드 이력</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
엣지 디바이스에 발행한 원격 커맨드 실행 로그
|
|
</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={load} disabled={loading} className="gap-1">
|
|
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
|
새로고침
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto px-6 pb-6">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : commands.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground rounded-xl border border-dashed">
|
|
<Terminal className="h-10 w-10 mb-3" />
|
|
<p className="text-sm">커맨드 이력이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-lg border overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted/50">
|
|
<tr>
|
|
<th className="text-left p-3 font-medium">ID</th>
|
|
<th className="text-left p-3 font-medium">디바이스</th>
|
|
<th className="text-left p-3 font-medium">커맨드</th>
|
|
<th className="text-left p-3 font-medium">상태</th>
|
|
<th className="text-left p-3 font-medium">발행자</th>
|
|
<th className="text-left p-3 font-medium">발행 시각</th>
|
|
<th className="text-left p-3 font-medium">응답</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{commands.map((c) => (
|
|
<tr key={c.id} className="border-t hover:bg-muted/30">
|
|
<td className="p-3 font-mono text-xs">#{c.id}</td>
|
|
<td className="p-3 font-mono text-xs">{c.device_id}</td>
|
|
<td className="p-3">{c.command_type}</td>
|
|
<td className="p-3">
|
|
<Badge
|
|
variant="outline"
|
|
className={`${STATUS_COLORS[c.status || ""]} text-[10px]`}
|
|
>
|
|
{c.status}
|
|
</Badge>
|
|
</td>
|
|
<td className="p-3 text-xs text-muted-foreground">{c.issued_by || "-"}</td>
|
|
<td className="p-3 text-xs text-muted-foreground">
|
|
{c.issued_at ? new Date(c.issued_at).toLocaleString("ko-KR") : "-"}
|
|
</td>
|
|
<td className="p-3 text-xs text-muted-foreground">
|
|
{c.error_message ? (
|
|
<span className="text-red-600">{c.error_message}</span>
|
|
) : c.responded_at ? (
|
|
new Date(c.responded_at).toLocaleString("ko-KR")
|
|
) : (
|
|
"-"
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|