This commit is contained in:
@@ -24,6 +24,25 @@ function eventTone(type?: string) {
|
|||||||
return 'neutral';
|
return 'neutral';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventRanges = [
|
||||||
|
{ label: '1시간', value: '1h', milliseconds: 60 * 60 * 1000 },
|
||||||
|
{ label: '24시간', value: '24h', milliseconds: 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: '전체', value: 'all', milliseconds: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEventInRange(event: RuntimeEvent, range: (typeof eventRanges)[number]) {
|
||||||
|
if (!range.milliseconds) return true;
|
||||||
|
const time = eventTime(event);
|
||||||
|
return time > 0 && time >= Date.now() - range.milliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
function metricKey(namespace: string, pod: string) {
|
function metricKey(namespace: string, pod: string) {
|
||||||
return `${namespace}/${pod}`;
|
return `${namespace}/${pod}`;
|
||||||
}
|
}
|
||||||
@@ -125,6 +144,7 @@ export function CommonAppLogsPage() {
|
|||||||
const [tail, setTail] = useState(100);
|
const [tail, setTail] = useState(100);
|
||||||
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [eventRange, setEventRange] = useState('1h');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -142,7 +162,9 @@ export function CommonAppLogsPage() {
|
|||||||
const componentResources = selectedComponent ? resources.filter((resource) => relatedToComponent(resource, selectedComponent)) : [];
|
const componentResources = selectedComponent ? resources.filter((resource) => relatedToComponent(resource, selectedComponent)) : [];
|
||||||
const loggableResources = componentResources.filter(isActiveLogTarget);
|
const loggableResources = componentResources.filter(isActiveLogTarget);
|
||||||
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
||||||
|
const selectedEventRange = eventRanges.find((range) => range.value === eventRange) ?? eventRanges[0];
|
||||||
const filteredEvents = events.filter((event) => {
|
const filteredEvents = events.filter((event) => {
|
||||||
|
if (!isEventInRange(event, selectedEventRange)) return false;
|
||||||
const normalizedSearch = search.trim().toLowerCase();
|
const normalizedSearch = search.trim().toLowerCase();
|
||||||
const componentMatch = selectedComponent
|
const componentMatch = selectedComponent
|
||||||
? `${event.involvedObject ?? ''} ${event.message ?? ''}`.toLowerCase().includes(selectedComponent.workloadName.toLowerCase())
|
? `${event.involvedObject ?? ''} ${event.message ?? ''}`.toLowerCase().includes(selectedComponent.workloadName.toLowerCase())
|
||||||
@@ -197,6 +219,26 @@ export function CommonAppLogsPage() {
|
|||||||
refresh();
|
refresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== 'events') return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadEvents = () => {
|
||||||
|
loadRuntimeEvents(COMMON_APP_NAMESPACE)
|
||||||
|
.then((nextEvents) => {
|
||||||
|
if (!cancelled) setEvents(nextEvents);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadEvents();
|
||||||
|
const timer = window.setInterval(loadEvents, 10_000);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -329,14 +371,28 @@ export function CommonAppLogsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full lg:w-80">
|
<div className="flex w-full flex-col gap-2 lg:w-auto lg:flex-row lg:items-center">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<div className="flex flex-wrap gap-2">
|
||||||
<input
|
{eventRanges.map((range) => (
|
||||||
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
<Button
|
||||||
placeholder="이벤트 검색"
|
key={range.value}
|
||||||
value={search}
|
variant={eventRange === range.value ? 'secondary' : 'outline'}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
className="h-8 px-3 text-[13px]"
|
||||||
/>
|
onClick={() => setEventRange(range.value)}
|
||||||
|
>
|
||||||
|
{range.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full lg:w-80">
|
||||||
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="이벤트 검색"
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,6 +53,25 @@ function eventTone(type?: string) {
|
|||||||
return 'neutral';
|
return 'neutral';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventRanges = [
|
||||||
|
{ label: '1시간', value: '1h', milliseconds: 60 * 60 * 1000 },
|
||||||
|
{ label: '24시간', value: '24h', milliseconds: 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: '전체', value: 'all', milliseconds: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEventInRange(event: RuntimeEvent, range: (typeof eventRanges)[number]) {
|
||||||
|
if (!range.milliseconds) return true;
|
||||||
|
const time = eventTime(event);
|
||||||
|
return time > 0 && time >= Date.now() - range.milliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
function metricKey(namespace: string, pod: string) {
|
function metricKey(namespace: string, pod: string) {
|
||||||
return `${namespace}/${pod}`;
|
return `${namespace}/${pod}`;
|
||||||
}
|
}
|
||||||
@@ -155,6 +174,7 @@ export function LauncherLogsPage() {
|
|||||||
const [tail, setTail] = useState(100);
|
const [tail, setTail] = useState(100);
|
||||||
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [eventRange, setEventRange] = useState('1h');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -172,7 +192,9 @@ export function LauncherLogsPage() {
|
|||||||
const componentResources = selectedComponent ? resources.filter((resource) => relatedToComponent(resource, selectedComponent)) : [];
|
const componentResources = selectedComponent ? resources.filter((resource) => relatedToComponent(resource, selectedComponent)) : [];
|
||||||
const loggableResources = componentResources.filter(isActiveLogTarget);
|
const loggableResources = componentResources.filter(isActiveLogTarget);
|
||||||
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
||||||
|
const selectedEventRange = eventRanges.find((range) => range.value === eventRange) ?? eventRanges[0];
|
||||||
const filteredEvents = events.filter((event) => {
|
const filteredEvents = events.filter((event) => {
|
||||||
|
if (!isEventInRange(event, selectedEventRange)) return false;
|
||||||
const normalizedSearch = search.trim().toLowerCase();
|
const normalizedSearch = search.trim().toLowerCase();
|
||||||
const componentText = `${event.involvedObject ?? ''} ${event.message ?? ''}`.toLowerCase();
|
const componentText = `${event.involvedObject ?? ''} ${event.message ?? ''}`.toLowerCase();
|
||||||
const componentMatch = selectedComponent ? componentText.includes(selectedComponent.workloadName.toLowerCase()) : true;
|
const componentMatch = selectedComponent ? componentText.includes(selectedComponent.workloadName.toLowerCase()) : true;
|
||||||
@@ -226,6 +248,26 @@ export function LauncherLogsPage() {
|
|||||||
refresh();
|
refresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== 'events') return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadEvents = () => {
|
||||||
|
loadRuntimeEvents(LAUNCHER_NAMESPACE)
|
||||||
|
.then((nextEvents) => {
|
||||||
|
if (!cancelled) setEvents(nextEvents);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadEvents();
|
||||||
|
const timer = window.setInterval(loadEvents, 10_000);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -358,14 +400,28 @@ export function LauncherLogsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full lg:w-80">
|
<div className="flex w-full flex-col gap-2 lg:w-auto lg:flex-row lg:items-center">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<div className="flex flex-wrap gap-2">
|
||||||
<input
|
{eventRanges.map((range) => (
|
||||||
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
<Button
|
||||||
placeholder="이벤트 검색"
|
key={range.value}
|
||||||
value={search}
|
variant={eventRange === range.value ? 'secondary' : 'outline'}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
className="h-8 px-3 text-[13px]"
|
||||||
/>
|
onClick={() => setEventRange(range.value)}
|
||||||
|
>
|
||||||
|
{range.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full lg:w-80">
|
||||||
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="이벤트 검색"
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,25 @@ function eventTone(type?: string) {
|
|||||||
return 'neutral';
|
return 'neutral';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventRanges = [
|
||||||
|
{ label: '1시간', value: '1h', milliseconds: 60 * 60 * 1000 },
|
||||||
|
{ label: '24시간', value: '24h', milliseconds: 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: '전체', value: 'all', milliseconds: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEventInRange(event: RuntimeEvent, range: (typeof eventRanges)[number]) {
|
||||||
|
if (!range.milliseconds) return true;
|
||||||
|
const time = eventTime(event);
|
||||||
|
return time > 0 && time >= Date.now() - range.milliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
function apiKind(kind: string) {
|
function apiKind(kind: string) {
|
||||||
return kind.toLowerCase();
|
return kind.toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -128,6 +147,7 @@ export function RuntimeLogsPage() {
|
|||||||
const [tail, setTail] = useState(100);
|
const [tail, setTail] = useState(100);
|
||||||
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
const [activeTab, setActiveTab] = useState<'logs' | 'events'>('logs');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [eventRange, setEventRange] = useState('1h');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -147,7 +167,9 @@ export function RuntimeLogsPage() {
|
|||||||
}, [podMetrics]);
|
}, [podMetrics]);
|
||||||
const loggableResources = resources.filter(isActiveLogTarget);
|
const loggableResources = resources.filter(isActiveLogTarget);
|
||||||
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
const selectedMetric = selectedResource?.kind === 'Pod' ? metricByPod.get(metricKey(activeNamespace, selectedResource.name)) : undefined;
|
||||||
|
const selectedEventRange = eventRanges.find((range) => range.value === eventRange) ?? eventRanges[0];
|
||||||
const filteredEvents = events.filter((event) => {
|
const filteredEvents = events.filter((event) => {
|
||||||
|
if (!isEventInRange(event, selectedEventRange)) return false;
|
||||||
const normalizedSearch = search.trim().toLowerCase();
|
const normalizedSearch = search.trim().toLowerCase();
|
||||||
if (!normalizedSearch) return true;
|
if (!normalizedSearch) return true;
|
||||||
return `${event.type ?? ''} ${event.reason ?? ''} ${event.message ?? ''} ${event.involvedObject ?? ''}`.toLowerCase().includes(normalizedSearch);
|
return `${event.type ?? ''} ${event.reason ?? ''} ${event.message ?? ''} ${event.involvedObject ?? ''}`.toLowerCase().includes(normalizedSearch);
|
||||||
@@ -217,6 +239,26 @@ export function RuntimeLogsPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== 'events' || !namespace.trim()) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadEvents = () => {
|
||||||
|
loadRuntimeEvents(namespace.trim())
|
||||||
|
.then((nextEvents) => {
|
||||||
|
if (!cancelled) setEvents(nextEvents);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadEvents();
|
||||||
|
const timer = window.setInterval(loadEvents, 10_000);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [activeTab, namespace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -363,14 +405,28 @@ export function RuntimeLogsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full lg:w-80">
|
<div className="flex w-full flex-col gap-2 lg:w-auto lg:flex-row lg:items-center">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<div className="flex flex-wrap gap-2">
|
||||||
<input
|
{eventRanges.map((range) => (
|
||||||
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
<Button
|
||||||
placeholder="이벤트 검색"
|
key={range.value}
|
||||||
value={search}
|
variant={eventRange === range.value ? 'secondary' : 'outline'}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
className="h-8 px-3 text-[13px]"
|
||||||
/>
|
onClick={() => setEventRange(range.value)}
|
||||||
|
>
|
||||||
|
{range.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full lg:w-80">
|
||||||
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
className="h-8 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="이벤트 검색"
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user