add event range filters to log event views
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 15:10:47 +09:00
parent 30a16e8076
commit daee96f2f6
3 changed files with 192 additions and 24 deletions
+64 -8
View File
@@ -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>
+64 -8
View File
@@ -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>
+64 -8
View File
@@ -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>