From 79a7d0e3654ff5a9c6568075ef0e9a8c8f9d0808 Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 9 Jul 2026 13:21:35 +0900 Subject: [PATCH] refine monitoring app selection and event filtering --- src/pages/system/MonitoringPage.tsx | 151 +++++++++++++++++++++------- 1 file changed, 112 insertions(+), 39 deletions(-) diff --git a/src/pages/system/MonitoringPage.tsx b/src/pages/system/MonitoringPage.tsx index f764d23..73187e5 100644 --- a/src/pages/system/MonitoringPage.tsx +++ b/src/pages/system/MonitoringPage.tsx @@ -14,6 +14,7 @@ import { } from 'recharts'; import { Badge, Button, Card, CardHeader } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; +import { getStoredSelectedAppId, setStoredSelectedAppId } from '../../lib/selected-app'; import { runtimeStatusDotClass } from '../../lib/status-dot'; import { cn } from '../../lib/utils'; import type { ApiApp, RuntimeEvent, RuntimePodMetric, RuntimeResource } from '../../types/runtime'; @@ -25,6 +26,20 @@ type ResourceRow = RuntimeResource & { }; const chartColors = ['#34d399', '#60a5fa', '#fbbf24', '#fb7185', '#a78bfa']; +const commonAppMonitoringAppId = '__common_apps__'; +const commonAppMonitoringApp: ApiApp = { + id: commonAppMonitoringAppId, + key: 'studio', + name: '공통앱', + namespace: 'studio-system', + status: 'ACTIVE' +}; +const eventRanges = [ + { label: '최근 10분', value: '10m', milliseconds: 10 * 60 * 1000 }, + { label: '1시간', value: '1h', milliseconds: 60 * 60 * 1000 }, + { label: '6시간', value: '6h', milliseconds: 6 * 60 * 60 * 1000 }, + { label: '24시간', value: '24h', milliseconds: 24 * 60 * 60 * 1000 } +]; function parseCpuMillicores(value?: string) { if (!value) return 0; @@ -60,22 +75,46 @@ function statusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'bl return 'neutral'; } +function isCompletedMigration(resource: RuntimeResource) { + const normalizedName = resource.name.toLowerCase(); + const normalizedStatus = resource.status.toLowerCase(); + return normalizedName.includes('migration') && ['completed', 'succeeded', 'success'].includes(normalizedStatus); +} + +function eventTime(event: RuntimeEvent) { + const value = event.lastTimestamp ?? event.firstTimestamp; + if (!value) return 0; + const time = new Date(value).getTime(); + return Number.isFinite(time) ? time : 0; +} + export function MonitoringPage() { const [apps, setApps] = useState([]); - const [selectedAppId, setSelectedAppId] = useState(''); + const [selectedAppId, setSelectedAppId] = useState(() => getStoredSelectedAppId()); + const [eventRange, setEventRange] = useState('1h'); const [resources, setResources] = useState([]); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const loadingVisible = useMinimumLoading(loading); const selectedApp = apps.find((app) => app.id === selectedAppId) ?? apps[0]; + const selectedRange = eventRanges.find((range) => range.value === eventRange) ?? eventRanges[1]; + const visibleResources = useMemo(() => resources.filter((resource) => !isCompletedMigration(resource)), [resources]); + const filteredEvents = useMemo(() => { + const threshold = Date.now() - selectedRange.milliseconds; + return events.filter((event) => eventTime(event) >= threshold); + }, [events, selectedRange.milliseconds]); const refresh = () => { setLoading(true); loadApps() .then(async (nextApps) => { - const appItems = nextApps.filter((app) => app.namespace); - const app = appItems.find((item) => item.id === selectedAppId) ?? appItems[0]; + const appItems = [commonAppMonitoringApp, ...nextApps.filter((app) => app.namespace)]; + const storedAppId = getStoredSelectedAppId(); + const preferredAppId = selectedAppId || storedAppId; + const app = appItems.find((item) => item.id === preferredAppId) + ?? appItems.find((item) => item.id === storedAppId) + ?? appItems[0]; setApps(appItems); setSelectedAppId(app?.id ?? ''); @@ -147,26 +186,26 @@ export function MonitoringPage() { }, [selectedApp?.namespace]); const summary = useMemo(() => { - const runtimeResources = resources.filter((resource) => ['Pod', 'Deployment', 'StatefulSet', 'DaemonSet'].includes(resource.kind)); + const runtimeResources = visibleResources.filter((resource) => ['Pod', 'Deployment', 'StatefulSet', 'DaemonSet'].includes(resource.kind)); const healthy = runtimeResources.filter(isHealthy).length; - const warnings = events.filter((event) => event.type === 'Warning').length; - const restarts = resources.reduce((sum, resource) => sum + (resource.restarts ?? 0), 0); + const warnings = filteredEvents.filter((event) => event.type === 'Warning').length; + const restarts = visibleResources.reduce((sum, resource) => sum + (resource.restarts ?? 0), 0); return { total: runtimeResources.length, healthy, warnings, restarts }; - }, [events, resources]); + }, [filteredEvents, visibleResources]); - const podMetrics = useMemo(() => resources + const podMetrics = useMemo(() => visibleResources .filter((resource) => resource.kind === 'Pod' && resource.metric) .map((resource) => ({ name: resource.name.replace(`${selectedApp?.key?.toLowerCase() ?? ''}-`, ''), cpu: parseCpuMillicores(resource.metric?.cpu), memory: Math.round(parseMemoryMi(resource.metric?.memory)) - })), [resources, selectedApp?.key]); + })), [visibleResources, selectedApp?.key]); const statusData = useMemo(() => { const byStatus = new Map(); - resources.forEach((resource) => byStatus.set(resource.status || '-', (byStatus.get(resource.status || '-') ?? 0) + 1)); + visibleResources.forEach((resource) => byStatus.set(resource.status || '-', (byStatus.get(resource.status || '-') ?? 0) + 1)); return [...byStatus.entries()].map(([name, value]) => ({ name, value })); - }, [resources]); + }, [visibleResources]); return ( <> @@ -183,25 +222,46 @@ export function MonitoringPage() { /> - +
+ +
+

검색 기간

+
+ {eventRanges.map((range) => ( + + ))} +
+
+
- resource.kind === 'Pod').length)} hint="실행 Pod 수" tone="blue" /> - 0 ? 'amber' : 'green'} /> - 0 ? 'amber' : 'green'} /> + resource.kind === 'Pod').length)} hint="현재 기준" tone="blue" /> + 0 ? 'amber' : 'green'} /> + 0 ? 'amber' : 'green'} />
@@ -223,15 +283,28 @@ export function MonitoringPage() { -
- - - - {statusData.map((entry, index) => )} - - - - +
+
+ + + + {statusData.map((entry, index) => )} + + + + +
+
+ {statusData.map((entry, index) => ( +
+ + + {entry.name} + + {entry.value} +
+ ))} +
@@ -240,7 +313,7 @@ export function MonitoringPage() {
- {resources.map((resource) => ( + {visibleResources.map((resource) => (
@@ -256,9 +329,9 @@ export function MonitoringPage() { - +
- {events.slice(0, 12).map((event, index) => ( + {filteredEvents.slice(0, 12).map((event, index) => (
{event.type ?? '-'} @@ -267,7 +340,7 @@ export function MonitoringPage() {

{event.message ?? '-'}

))} - {!events.length ?
표시할 이벤트가 없습니다.
: null} + {!filteredEvents.length ?
선택 기간에 표시할 이벤트가 없습니다.
: null}