merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 15:05:28 +09:00
3 changed files with 74 additions and 12 deletions
+2 -2
View File
@@ -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
View File
@@ -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: '장비 제어' },
+64 -3
View File
@@ -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">