feat(ui): startup logs — sort toggle + refresh button + desc default

This commit is contained in:
hsiegeln
2026-04-23 16:00:44 +02:00
parent 1d7009d69c
commit fb7b15f539
3 changed files with 145 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
import { LogViewer } from '@cameleer/design-system';
import { useRef, useState } from 'react';
import { LogViewer, Button } from '@cameleer/design-system';
import type { LogEntry } from '@cameleer/design-system';
import { RefreshCw } from 'lucide-react';
import { useStartupLogs } from '../api/queries/logs';
import type { Deployment } from '../api/queries/admin/apps';
import styles from './StartupLogPanel.module.css';
@@ -14,10 +16,24 @@ interface StartupLogPanelProps {
export function StartupLogPanel({ deployment, appSlug, envSlug, className }: StartupLogPanelProps) {
const isStarting = deployment.status === 'STARTING';
const isFailed = deployment.status === 'FAILED';
const [sort, setSort] = useState<'asc' | 'desc'>('desc');
const scrollRef = useRef<HTMLDivElement>(null);
const { data } = useStartupLogs(appSlug, envSlug, deployment.createdAt, isStarting);
const query = useStartupLogs(appSlug, envSlug, deployment.createdAt, isStarting, sort);
const entries = query.data?.data ?? [];
const entries = data?.data ?? [];
const scrollToLatest = () => {
const el = scrollRef.current;
if (!el) return;
// asc → latest at bottom; desc → latest at top
const top = sort === 'asc' ? el.scrollHeight : 0;
el.scrollTo({ top, behavior: 'smooth' });
};
const handleRefresh = async () => {
await query.refetch?.();
scrollToLatest();
};
if (entries.length === 0 && !isStarting) return null;
@@ -36,13 +52,35 @@ export function StartupLogPanel({ deployment, appSlug, envSlug, className }: Sta
<span className={`${styles.badge} ${styles.badgeStopped}`}>stopped</span>
)}
</div>
<span className={styles.lineCount}>{entries.length} lines</span>
<div className={styles.headerRight}>
<span className={styles.lineCount}>{entries.length} entries</span>
<Button
variant="ghost"
size="sm"
onClick={() => setSort((s) => (s === 'asc' ? 'desc' : 'asc'))}
title={sort === 'asc' ? 'Oldest first' : 'Newest first'}
aria-label={sort === 'asc' ? 'Oldest first' : 'Newest first'}
>
{sort === 'asc' ? '\u2191' : '\u2193'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
title="Refresh"
aria-label="Refresh"
>
<RefreshCw size={14} />
</Button>
</div>
</div>
<div ref={scrollRef} className={styles.scrollWrap}>
{entries.length > 0 ? (
<LogViewer entries={entries as unknown as LogEntry[]} />
) : (
<div className={styles.empty}>Waiting for container output...</div>
)}
</div>
{entries.length > 0 ? (
<LogViewer entries={entries as unknown as LogEntry[]} />
) : (
<div className={styles.empty}>Waiting for container output...</div>
)}
</div>
);
}