From 2efc09d58769cb2c7753715ba9a78f787b3a85a4 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Mon, 8 Jun 2026 08:38:12 +0300 Subject: [PATCH] refactor: improve log details view --- ui/app/components/LogDetailModal.vue | 509 +++++++++++++++++++++++++++ ui/app/pages/logs.vue | 414 +--------------------- 2 files changed, 511 insertions(+), 412 deletions(-) create mode 100644 ui/app/components/LogDetailModal.vue diff --git a/ui/app/components/LogDetailModal.vue b/ui/app/components/LogDetailModal.vue new file mode 100644 index 00000000..8a9e34f8 --- /dev/null +++ b/ui/app/components/LogDetailModal.vue @@ -0,0 +1,509 @@ + + + diff --git a/ui/app/pages/logs.vue b/ui/app/pages/logs.vue index bd66e077..e424fbdf 100644 --- a/ui/app/pages/logs.vue +++ b/ui/app/pages/logs.vue @@ -224,221 +224,7 @@ - - - + @@ -448,7 +234,7 @@ import type { EventSourceMessage } from '@microsoft/fetch-event-source'; import moment from 'moment'; import { useStorage } from '@vueuse/core'; import type { log_line } from '~/types/logs'; -import { copyText, parse_api_error, request, uri } from '~/utils'; +import { parse_api_error, request, uri } from '~/utils'; import { requirePageShell } from '~/utils/topLevelNavigation'; type FilteredLogEntry = { @@ -459,19 +245,6 @@ type FilteredLogEntry = { }; type LogLevel = 'debug' | 'info' | 'warning' | 'error'; -type LogLevelColor = 'neutral' | 'info' | 'warning' | 'error'; -type DetailRow = { - label: string; - value: string; - icon: string; -}; -type LogFieldRow = { - key: string; - label: string; - value: string; - preview: string; - kind: 'scalar' | 'text' | 'json'; -}; type LevelFilterItem = { label: string; value: LogLevel; @@ -481,12 +254,6 @@ const FILTER_CONTEXT_REGEX = /context:(\d+)/; const LOG_LEVELS: LogLevel[] = ['debug', 'info', 'warning', 'error']; const LOG_ROW_CLASS = 'flex min-w-0 border-b border-default/40 bg-transparent transition-colors duration-150 last:border-b-0 hover:bg-elevated/70'; -const LOG_LEVEL_COLOR: Record = { - debug: 'neutral', - info: 'info', - warning: 'warning', - error: 'error', -}; const LOG_LEVEL_ICON: Record = { debug: 'i-lucide-terminal', info: 'i-lucide-info', @@ -507,12 +274,6 @@ const pageShell = requirePageShell('logs'); const logContainer = useTemplateRef('logContainer'); const textWrap = useStorage('logs_wrap', true); -const exceptionOpen = useStorage('logs_exception_open', false); -const fieldsOpen = useStorage('logs_fields_open', true); -const rawJsonOpen = useStorage('logs_raw_json_open', false); -const sourceOpen = useStorage('logs_source_open', true); -const fieldOpenState = ref>({}); -const fieldFilters = ref>({}); const selectedLevels = useStorage('logs_level_filter', [...LOG_LEVELS]); const sseController = ref(null); const runtimeLogLevel = ref(null); @@ -532,33 +293,6 @@ const pageCardUi = { wrapper: 'w-full min-w-0 items-stretch', body: 'w-full min-w-0 max-w-full overflow-hidden', }; -const detailsModalUi = { - content: 'max-w-5xl', - body: 'max-h-[75vh] overflow-y-auto', -}; - -const copyMenuItems = computed(() => [ - [ - { - label: 'Copy Message', - icon: 'i-lucide-message-square-text', - onSelect: () => { - if (selectedLog.value) { - copyText(selectedLog.value.message); - } - }, - }, - { - label: 'Copy JSON', - icon: 'i-lucide-braces', - onSelect: () => { - if (selectedLog.value) { - copyText(logRaw(selectedLog.value)); - } - }, - }, - ], -]); const query = ref( (() => { @@ -968,8 +702,6 @@ const logTimeLabel = (value?: string): string => const logTimeTitle = (value?: string): string => value ? moment(value).format('YYYY-MM-DD HH:mm:ss Z') : 'No timestamp'; -const logRaw = (log: log_line): string => JSON.stringify(log, null, 2); - const exceptionSummary = (log: log_line): string => { const type = log.exception?.type?.trim() ?? ''; const message = log.exception?.message?.trim() ?? ''; @@ -981,9 +713,6 @@ const exceptionSummary = (log: log_line): string => { return type || message; }; -const exceptionText = (log: log_line): string => - log.exception ? JSON.stringify(log.exception, null, 2) : ''; - const searchableLog = (log: log_line): string => [ log.message, @@ -1057,145 +786,6 @@ const openLogDetails = (log: log_line): void => { detailsOpen.value = true; }; -const formatDetailValue = (value: unknown): string => { - if (value === undefined || value === null || value === '') { - return ''; - } - - if (typeof value === 'string') { - return value; - } - - if (typeof value === 'number' || typeof value === 'boolean') { - return String(value); - } - - return JSON.stringify(value); -}; - -const compactRows = (rows: Array<{ label: string; value: unknown; icon: string }>): DetailRow[] => - rows - .map((row) => ({ - label: row.label, - value: formatDetailValue(row.value), - icon: row.icon, - })) - .filter((row) => Boolean(row.value)); - -const formatNameId = (name: unknown, id: unknown): string => { - const nameValue = formatDetailValue(name); - const idValue = formatDetailValue(id); - if (nameValue && idValue) { - return `${nameValue} / ${idValue}`; - } - - return nameValue || idValue; -}; - -const detailRows = (log: log_line): DetailRow[] => - compactRows([ - { label: 'File', value: log.source?.file, icon: 'i-lucide-file' }, - { label: 'Line', value: log.source?.line, icon: 'i-lucide-hash' }, - { label: 'Function', value: log.source?.function, icon: 'i-lucide-code-2' }, - { label: 'Module', value: log.source?.module, icon: 'i-lucide-box' }, - { label: 'Path', value: log.source?.path, icon: 'i-lucide-folder-tree' }, - { - label: 'Process / ID', - value: formatNameId(log.process?.name, log.process?.id), - icon: 'i-lucide-cpu', - }, - { - label: 'Thread / ID', - value: formatNameId(log.thread?.name, log.thread?.id), - icon: 'i-lucide-git-branch', - }, - ]); - -const fieldRows = (log: log_line): LogFieldRow[] => { - if (!log.fields) return []; - const rows: LogFieldRow[] = []; - for (const [key, rawValue] of Object.entries(log.fields)) { - if (rawValue === undefined || rawValue === null || rawValue === '') continue; - const jsonValue = - typeof rawValue === 'string' - ? parseJsonContainerString(rawValue) - : isJsonContainer(rawValue) - ? rawValue - : null; - const value = jsonValue ? JSON.stringify(jsonValue, null, 2) : formatFieldValue(rawValue); - const kind = jsonValue - ? 'json' - : typeof rawValue === 'string' - ? rawValue.includes('\n') - ? 'text' - : 'scalar' - : 'scalar'; - rows.push({ - key, - label: normalizeFieldLabel(key), - value, - preview: formatFieldValue(rawValue).replaceAll('\n', ' '), - kind, - }); - } - return rows; -}; - -const fieldOpen = (key: string) => fieldOpenState.value[key] ?? false; - -const toggleField = (key: string) => { - fieldOpenState.value = { ...fieldOpenState.value, [key]: !fieldOpen(key) }; -}; - -const fieldFilter = (key: string) => fieldFilters.value[key] ?? ''; - -const setFieldFilter = (key: string, value: string | number) => { - fieldFilters.value = { ...fieldFilters.value, [key]: String(value ?? '') }; -}; - -const displayedFieldValue = (field: LogFieldRow): string => { - const query = fieldFilter(field.key); - if (!query) return field.value; - const needle = query.toLowerCase(); - return field.value.split('\n').filter((line) => line.toLowerCase().includes(needle)).join('\n'); -}; - -const copyFieldValue = (field: LogFieldRow): void => { - copyText(field.value); -}; - -const isJsonLike = (value: string): boolean => { - const trimmed = value.trim(); - return ( - ((trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']'))) && - trimmed.length > 1 - ); -}; - -const isJsonContainer = (value: unknown): value is Record | unknown[] => - Array.isArray(value) || (!!value && typeof value === 'object'); - -const parseJsonContainerString = (value: string): Record | unknown[] | null => { - if (!isJsonLike(value)) return null; - try { - const parsed = JSON.parse(value) as unknown; - if (Array.isArray(parsed)) return parsed; - if (parsed && typeof parsed === 'object') return parsed as Record; - } catch { - return null; - } - return null; -}; - -const normalizeFieldLabel = (key: string) => key.replaceAll('_', ' '); - -const formatFieldValue = (value: unknown): string => { - if (typeof value === 'string') return value; - if (typeof value === 'number' || typeof value === 'boolean') return String(value); - return JSON.stringify(value, null, 2); -}; - onMounted(async () => { if (!config.app.file_logging) { await navigateTo('/');