From 06137f3d77c43581a6ed2074f5d5f3c27514d374 Mon Sep 17 00:00:00 2001 From: chyoo Date: Wed, 8 Jul 2026 16:39:09 +0900 Subject: [PATCH] connect tailscale node status through platform gateway --- src/pages/RestoredPages.tsx | 98 +++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/src/pages/RestoredPages.tsx b/src/pages/RestoredPages.tsx index 8be4ba1..9e07399 100644 --- a/src/pages/RestoredPages.tsx +++ b/src/pages/RestoredPages.tsx @@ -1,5 +1,5 @@ import { ChevronDown, ChevronUp, MoreHorizontal, Plus, RefreshCcw, Settings2, SlidersHorizontal } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { DataTableCard, type DataTableColumn } from '../components/data-table'; import { Badge, Button, Card, CardHeader, Modal, Progress } from '../components/ui'; import { @@ -11,9 +11,10 @@ import { launcherCommonSettings, launcherVersions, runtimeCommands, - tailscaleNodes, + tailscaleNodes as mockTailscaleNodes, tailscaleSettings } from '../data/mock'; +import { apiRequest } from '../lib/api'; import { PageTitle } from '../layout/AppLayout'; type Tone = 'neutral' | 'green' | 'amber' | 'red' | 'blue' | 'violet'; @@ -29,6 +30,37 @@ function StatusBadge({ status, badgeTone }: { status: string; badgeTone?: Tone } return {status}; } +type TailscaleNode = { + id: string; + name: string; + ip: string; + ips?: string[]; + role: string; + user: string; + routes: string; + lastSeen: string | null; + status: string; + online?: boolean; +}; + +type TailscaleNodesResponse = { + nodes: TailscaleNode[]; + source?: string; + fetchedAt?: string; + error?: string; +}; + +function formatTailscaleLastSeen(value: string | null) { + if (!value) return '-'; + const time = Date.parse(value); + if (!Number.isFinite(time)) return value; + const diff = Date.now() - time; + if (diff < 60_000) return '방금 전'; + if (diff < 60 * 60_000) return `${Math.floor(diff / 60_000)}분 전`; + if (diff < 24 * 60 * 60_000) return `${Math.floor(diff / (60 * 60_000))}시간 전`; + return new Date(time).toLocaleString(); +} + export function LauncherVersionsPage() { const [sourceOpen, setSourceOpen] = useState(false); const [releaseOpen, setReleaseOpen] = useState(false); @@ -351,25 +383,48 @@ export function LauncherCommonSettingsPage() { } export function TailscaleStatusPage() { - const online = tailscaleNodes.filter((node) => node.status === '활성').length; - const subnetRouters = tailscaleNodes.filter((node) => node.routes !== '-').length; + const [nodes, setNodes] = useState([]); + const [source, setSource] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const online = nodes.filter((node) => node.status === '활성' || node.online).length; + const subnetRouters = nodes.filter((node) => node.routes && node.routes !== '-').length; + + const refresh = () => { + setLoading(true); + setError(''); + apiRequest('/tailscale/nodes') + .then((result) => { + setNodes(result.nodes ?? []); + setSource(result.source ?? ''); + setError(result.error ?? ''); + }) + .catch((requestError: Error) => { + setError(requestError.message); + }) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + refresh(); + }, []); return ( <> - - 상태 갱신 + } />
{[ - ['등록 노드', `${tailscaleNodes.length}대`, '게이트웨이 수집 기준'], + ['등록 노드', `${nodes.length}대`, source ? `${source} 수집 기준` : '게이트웨이 수집 기준'], ['온라인', `${online}대`, '최근 접속 정상'], ['Subnet Router', `${subnetRouters}대`, '클러스터 CIDR 광고'] ].map(([label, value, description]) => ( @@ -380,14 +435,19 @@ export function TailscaleStatusPage() { ))}
+ {error ? ( + + {error} + + ) : null}
- +
); } -function TailscaleNodeTable() { +function TailscaleNodeTable({ nodes, loading }: { nodes: TailscaleNode[]; loading: boolean }) { return ( @@ -405,14 +465,24 @@ function TailscaleNodeTable() { - {tailscaleNodes.map((node) => ( + {loading && nodes.length === 0 ? ( + + 노드 정보를 불러오는 중입니다. + + ) : null} + {!loading && nodes.length === 0 ? ( + + 표시할 노드가 없습니다. + + ) : null} + {nodes.map((node) => ( {node.name} {node.ip} {node.role} {node.user} {node.routes} - {node.lastSeen} + {formatTailscaleLastSeen(node.lastSeen)} ))} @@ -424,7 +494,7 @@ function TailscaleNodeTable() { } export function TailscaleRoutesPage() { - const routes = tailscaleNodes.filter((node) => node.routes !== '-'); + const routes = mockTailscaleNodes.filter((node) => node.routes !== '-'); return ( <>