This commit is contained in:
+2
-2
@@ -963,8 +963,8 @@ function SkeletonBlock({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-md bg-white/10',
|
||||
'before:absolute before:inset-0 before:-translate-x-full before:animate-[skeleton-shimmer_1.35s_ease-in-out_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/25 before:to-transparent',
|
||||
'relative overflow-hidden rounded-md bg-white/15',
|
||||
'before:absolute before:inset-0 before:-translate-x-full before:animate-[skeleton-shimmer_1.35s_ease-in-out_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/35 before:to-transparent',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
MonitorPlay,
|
||||
Bell,
|
||||
Boxes,
|
||||
ChartNoAxesColumnIncreasing,
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Megaphone,
|
||||
MonitorUp,
|
||||
Network,
|
||||
Rocket,
|
||||
ServerCog,
|
||||
ShieldCheck,
|
||||
Users
|
||||
@@ -31,7 +32,7 @@ export const navGroups = [
|
||||
},
|
||||
{
|
||||
label: '런처',
|
||||
icon: Rocket,
|
||||
icon: MonitorPlay,
|
||||
children: [
|
||||
{ label: '소스 관리', view: 'launcherSources' },
|
||||
{ label: '빌드 관리', view: 'launcherBuilds' },
|
||||
@@ -67,7 +68,7 @@ export const navGroups = [
|
||||
},
|
||||
{
|
||||
label: '에이전트',
|
||||
icon: ShieldCheck,
|
||||
icon: MonitorUp,
|
||||
children: [
|
||||
{ label: '장비 등록' },
|
||||
{ label: '장비 제어' },
|
||||
|
||||
@@ -100,6 +100,7 @@ export function MonitoringPage() {
|
||||
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 showInitialSkeleton = loading && resources.length === 0 && events.length === 0;
|
||||
const filteredEvents = useMemo(() => {
|
||||
const threshold = Date.now() - selectedRange.milliseconds;
|
||||
return events.filter((event) => eventTime(event) >= threshold);
|
||||
@@ -258,10 +259,16 @@ export function MonitoringPage() {
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{showInitialSkeleton ? (
|
||||
Array.from({ length: 4 }).map((_, index) => <MetricCardSkeleton key={index} />)
|
||||
) : (
|
||||
<>
|
||||
<MetricCard icon={Server} label="워크로드" value={`${summary.healthy}/${summary.total}`} hint="Pod/Deployment/StatefulSet 기준" tone={summary.healthy === summary.total ? 'green' : 'amber'} />
|
||||
<MetricCard icon={Activity} label="Pod" value={String(visibleResources.filter((resource) => resource.kind === 'Pod').length)} hint="현재 기준" tone="blue" />
|
||||
<MetricCard icon={AlertTriangle} label="Warning 이벤트" value={String(summary.warnings)} hint={`${selectedRange.label} 기준`} tone={summary.warnings > 0 ? 'amber' : 'green'} />
|
||||
<MetricCard icon={RotateCcw} label="재시작" value={String(summary.restarts)} hint="현재 Pod 누적" tone={summary.restarts > 0 ? 'amber' : 'green'} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(360px,0.75fr)]">
|
||||
@@ -313,7 +320,9 @@ export function MonitoringPage() {
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader title="리소스 상태" description="선택한 앱 네임스페이스의 주요 리소스입니다." />
|
||||
<div className="max-h-[420px] divide-y divide-border overflow-y-auto">
|
||||
{visibleResources.map((resource) => (
|
||||
{showInitialSkeleton ? (
|
||||
<ListSkeleton rows={6} />
|
||||
) : visibleResources.map((resource) => (
|
||||
<div key={`${resource.kind}-${resource.name}`} className="flex items-center justify-between gap-4 px-5 py-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -331,7 +340,9 @@ export function MonitoringPage() {
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader title="이벤트 목록" description="선택한 기간의 Warning 이벤트를 우선 확인합니다." />
|
||||
<div className="max-h-[420px] divide-y divide-border overflow-y-auto">
|
||||
{filteredEvents.slice(0, 12).map((event, index) => (
|
||||
{showInitialSkeleton ? (
|
||||
<ListSkeleton rows={5} />
|
||||
) : filteredEvents.slice(0, 12).map((event, index) => (
|
||||
<div key={`${event.involvedObject}-${event.reason}-${index}`} className="px-5 py-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge tone={event.type === 'Warning' ? 'amber' : 'neutral'}>{event.type ?? '-'}</Badge>
|
||||
@@ -340,7 +351,7 @@ export function MonitoringPage() {
|
||||
<p className="mt-2 line-clamp-2 text-xs leading-5 text-muted-foreground">{event.message ?? '-'}</p>
|
||||
</div>
|
||||
))}
|
||||
{!filteredEvents.length ? <div className="px-5 py-12 text-center text-sm text-muted-foreground">선택 기간에 표시할 이벤트가 없습니다.</div> : null}
|
||||
{!showInitialSkeleton && !filteredEvents.length ? <div className="px-5 py-12 text-center text-sm text-muted-foreground">선택 기간에 표시할 이벤트가 없습니다.</div> : null}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -350,6 +361,56 @@ export function MonitoringPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonBlock({ className }: { className: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-md bg-white/15',
|
||||
'before:absolute before:inset-0 before:-translate-x-full before:animate-[skeleton-shimmer_1.35s_ease-in-out_infinite]',
|
||||
'before:bg-gradient-to-r before:from-transparent before:via-white/35 before:to-transparent',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCardSkeleton() {
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<SkeletonBlock className="h-4 w-20" />
|
||||
<SkeletonBlock className="size-4 rounded-full" />
|
||||
</div>
|
||||
<div className="mt-4 flex items-end justify-between gap-3">
|
||||
<SkeletonBlock className="h-9 w-24" />
|
||||
<SkeletonBlock className="h-6 w-12" />
|
||||
</div>
|
||||
<SkeletonBlock className="mt-3 h-3 w-36" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ListSkeleton({ rows }: { rows: number }) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: rows }).map((_, index) => (
|
||||
<div key={index} className="px-5 py-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonBlock className="size-2.5 rounded-full" />
|
||||
<SkeletonBlock className="h-4 w-3/5" />
|
||||
</div>
|
||||
<SkeletonBlock className="mt-2 h-3 w-2/5" />
|
||||
</div>
|
||||
<SkeletonBlock className="h-6 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ icon: Icon, label, value, hint, tone }: { icon: typeof Server; label: string; value: string; hint: string; tone: 'green' | 'amber' | 'red' | 'blue' | 'neutral' }) {
|
||||
return (
|
||||
<Card className="p-5">
|
||||
|
||||
Reference in New Issue
Block a user