feat: add fullscreen logs view

This commit is contained in:
HYec
2026-06-01 16:38:34 +00:00
Unverified
parent 75da30311a
commit 60092f3da0
7 changed files with 192 additions and 57 deletions
+22
View File
@@ -182,6 +182,28 @@ export function IconTrash2({ size = 20, ...props }: IconProps) {
);
}
export function IconMaximize2({ size = 20, ...props }: IconProps) {
return (
<svg {...baseSvgProps} width={size} height={size} {...props}>
<path d="M15 3h6v6" />
<path d="m21 3-7 7" />
<path d="M9 21H3v-6" />
<path d="m3 21 7-7" />
</svg>
);
}
export function IconMinimize2({ size = 20, ...props }: IconProps) {
return (
<svg {...baseSvgProps} width={size} height={size} {...props}>
<path d="M4 14h6v6" />
<path d="m10 14-7 7" />
<path d="M20 10h-6V4" />
<path d="m14 10 7-7" />
</svg>
);
}
export function IconPlus({ size = 20, ...props }: IconProps) {
return (
<svg {...baseSvgProps} width={size} height={size} {...props}>
+2
View File
@@ -732,6 +732,8 @@
"title": "Logs Viewer",
"refresh_button": "Refresh Logs",
"clear_button": "Clear Logs",
"fullscreen_button": "Full Screen",
"exit_fullscreen_button": "Exit Full Screen",
"download_button": "Download Logs",
"error_log_button": "Select Error Log",
"error_logs_modal_title": "Error Request Logs",
+2
View File
@@ -729,6 +729,8 @@
"title": "Просмотр журналов",
"refresh_button": "Обновить журналы",
"clear_button": "Очистить журналы",
"fullscreen_button": "На весь экран",
"exit_fullscreen_button": "Выйти из полноэкранного режима",
"download_button": "Скачать журналы",
"error_log_button": "Выбрать журнал ошибок",
"error_logs_modal_title": "Журналы ошибок запросов",
+2
View File
@@ -732,6 +732,8 @@
"title": "日志查看",
"refresh_button": "刷新日志",
"clear_button": "清空日志",
"fullscreen_button": "全屏显示",
"exit_fullscreen_button": "退出全屏",
"download_button": "下载日志",
"error_log_button": "选择错误日志",
"error_logs_modal_title": "错误请求日志",
+2
View File
@@ -758,6 +758,8 @@
"title": "記錄檢視",
"refresh_button": "重新整理記錄",
"clear_button": "清空記錄",
"fullscreen_button": "全螢幕顯示",
"exit_fullscreen_button": "退出全螢幕",
"download_button": "下載記錄",
"error_log_button": "選擇錯誤記錄",
"error_logs_modal_title": "錯誤請求記錄",
+44
View File
@@ -722,3 +722,47 @@
height: 280px;
}
}
.logCardFullscreen {
position: fixed;
inset: 0;
z-index: $z-modal;
min-height: 0;
width: 100vw;
height: 100vh;
padding: $spacing-lg;
border-radius: 0;
border: none;
overflow: hidden;
background: var(--bg-primary);
@include mobile {
padding: $spacing-md;
min-height: 0;
overflow: hidden;
}
}
:global(body.logs-fullscreen-active) :global(.page-transition__layer) {
transform: none !important;
}
.logPanelFullscreen {
flex: 1 1 auto;
min-height: 0;
height: auto;
max-height: none;
resize: none;
@include tablet {
min-height: 0;
max-height: none;
}
@include mobile {
flex: 1 1 auto;
min-height: 0;
height: auto;
max-height: none;
}
}
+118 -57
View File
@@ -7,12 +7,15 @@ import { EmptyState } from '@/components/ui/EmptyState';
import { Input } from '@/components/ui/Input';
import { Modal } from '@/components/ui/Modal';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { lockScroll, unlockScroll } from '@/components/ui/scrollLock';
import {
IconChevronDown,
IconChevronUp,
IconCode,
IconDownload,
IconEyeOff,
IconMaximize2,
IconMinimize2,
IconRefreshCw,
IconSearch,
IconSlidersHorizontal,
@@ -91,6 +94,7 @@ export function LogsPage() {
const [errorLogsError, setErrorLogsError] = useState('');
const [requestLogId, setRequestLogId] = useState<string | null>(null);
const [requestLogDownloading, setRequestLogDownloading] = useState(false);
const [fullscreenLogs, setFullscreenLogs] = useState(false);
const logScrollerRef = useRef<ReturnType<typeof useLogScroller> | null>(null);
const longPressRef = useRef<{
@@ -450,6 +454,27 @@ export function LogsPage() {
};
}, []);
useEffect(() => {
if (!fullscreenLogs) return;
document.body.classList.add('logs-fullscreen-active');
lockScroll();
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setFullscreenLogs(false);
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.classList.remove('logs-fullscreen-active');
unlockScroll();
};
}, [fullscreenLogs]);
return (
<div className={styles.container}>
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
@@ -465,7 +490,10 @@ export function LogsPage() {
<button
type="button"
className={`${styles.tabItem} ${activeTab === 'errors' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('errors')}
onClick={() => {
setFullscreenLogs(false);
setActiveTab('errors');
}}
>
{t('logs.error_logs_modal_title')}
</button>
@@ -473,67 +501,75 @@ export function LogsPage() {
<div className={styles.content}>
{activeTab === 'logs' && (
<Card className={styles.logCard}>
<Card
className={[styles.logCard, fullscreenLogs ? styles.logCardFullscreen : '']
.filter(Boolean)
.join(' ')}
>
{error && <div className="error-box">{error}</div>}
<div className={styles.filters}>
<div className={styles.searchWrapper}>
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('logs.search_placeholder')}
className={styles.searchInput}
rightElement={
searchQuery ? (
<button
type="button"
className={styles.searchClear}
onClick={() => setSearchQuery('')}
title="Clear"
aria-label="Clear"
>
<IconX size={16} />
</button>
) : (
<IconSearch size={16} className={styles.searchIcon} />
)
}
/>
</div>
{!fullscreenLogs && (
<>
<div className={styles.searchWrapper}>
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('logs.search_placeholder')}
className={styles.searchInput}
rightElement={
searchQuery ? (
<button
type="button"
className={styles.searchClear}
onClick={() => setSearchQuery('')}
title="Clear"
aria-label="Clear"
>
<IconX size={16} />
</button>
) : (
<IconSearch size={16} className={styles.searchIcon} />
)
}
/>
</div>
<div className={styles.filterPanelHeader}>
<Button
type="button"
variant="secondary"
size="sm"
className={styles.filterPanelToggle}
onClick={() => setStructuredFiltersExpanded((prev) => !prev)}
aria-expanded={structuredFiltersExpanded}
aria-controls={structuredFiltersPanelId}
title={
structuredFiltersExpanded
? t('logs.filter_panel_collapse')
: t('logs.filter_panel_expand')
}
>
<span className={styles.filterPanelButtonContent}>
<IconSlidersHorizontal size={16} />
<span>{t('logs.filter_panel_title')}</span>
{structuredFilterCount > 0 && (
<span className={styles.filterPanelCount}>
{t('logs.filter_panel_active_count', { count: structuredFilterCount })}
<div className={styles.filterPanelHeader}>
<Button
type="button"
variant="secondary"
size="sm"
className={styles.filterPanelToggle}
onClick={() => setStructuredFiltersExpanded((prev) => !prev)}
aria-expanded={structuredFiltersExpanded}
aria-controls={structuredFiltersPanelId}
title={
structuredFiltersExpanded
? t('logs.filter_panel_collapse')
: t('logs.filter_panel_expand')
}
>
<span className={styles.filterPanelButtonContent}>
<IconSlidersHorizontal size={16} />
<span>{t('logs.filter_panel_title')}</span>
{structuredFilterCount > 0 && (
<span className={styles.filterPanelCount}>
{t('logs.filter_panel_active_count', { count: structuredFilterCount })}
</span>
)}
{structuredFiltersExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)}
</span>
)}
{structuredFiltersExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)}
</span>
</Button>
</div>
</Button>
</div>
</>
)}
{structuredFiltersExpanded && (
{!fullscreenLogs && structuredFiltersExpanded && (
<div id={structuredFiltersPanelId} className={styles.structuredFilters}>
<div className={styles.filterChipGroup}>
<span className={styles.filterChipLabel}>{t('logs.filter_method')}</span>
@@ -690,6 +726,29 @@ export function LogsPage() {
{t('logs.clear_button')}
</span>
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => setFullscreenLogs((prev) => !prev)}
className={styles.actionButton}
aria-pressed={fullscreenLogs}
title={
fullscreenLogs
? t('logs.exit_fullscreen_button')
: t('logs.fullscreen_button')
}
>
<span className={styles.buttonContent}>
{fullscreenLogs ? (
<IconMinimize2 size={16} />
) : (
<IconMaximize2 size={16} />
)}
{fullscreenLogs
? t('logs.exit_fullscreen_button')
: t('logs.fullscreen_button')}
</span>
</Button>
</div>
</div>
@@ -698,7 +757,9 @@ export function LogsPage() {
) : logState.buffer.length > 0 && filteredLines.length > 0 ? (
<div
ref={scroller.logViewerRef}
className={styles.logPanel}
className={[styles.logPanel, fullscreenLogs ? styles.logPanelFullscreen : '']
.filter(Boolean)
.join(' ')}
onScroll={scroller.handleLogScroll}
>
{scroller.canLoadMore && (