|
14 | 14 | Clock, |
15 | 15 | Loader2, |
16 | 16 | Info, |
| 17 | + ChevronDown, |
17 | 18 | } from '@lucide/svelte'; |
18 | 19 | import { onMount } from 'svelte'; |
19 | 20 | import { t } from '$lib/i18n'; |
|
71 | 72 | let error = $state<string | null>(null); |
72 | 73 | let copied = $state(false); |
73 | 74 | let copyTimer: ReturnType<typeof setTimeout> | null = null; |
| 75 | + let expandedChecks = $state(new Set<string>()); |
| 76 | +
|
| 77 | + function toggleExpand(checkName: string) { |
| 78 | + const next = new Set(expandedChecks); |
| 79 | + if (next.has(checkName)) { |
| 80 | + next.delete(checkName); |
| 81 | + } else { |
| 82 | + next.add(checkName); |
| 83 | + } |
| 84 | + expandedChecks = next; |
| 85 | + } |
| 86 | +
|
| 87 | + interface ErrorGroup { |
| 88 | + component?: string; |
| 89 | + message: string; |
| 90 | + count: number; |
| 91 | + level: string; |
| 92 | + sample_fields?: Record<string, unknown>; |
| 93 | + } |
| 94 | +
|
| 95 | + function getTopErrors(result: DiagnosticsResult): ErrorGroup[] | null { |
| 96 | + const topErrors = result.details?.top_errors; |
| 97 | + if (!Array.isArray(topErrors) || topErrors.length === 0) return null; |
| 98 | + return topErrors as ErrorGroup[]; |
| 99 | + } |
| 100 | +
|
| 101 | + function levelColor(level: string): string { |
| 102 | + switch (level) { |
| 103 | + case 'fatal': |
| 104 | + case 'panic': |
| 105 | + case 'error': |
| 106 | + return 'var(--color-error)'; |
| 107 | + case 'warn': |
| 108 | + case 'warning': |
| 109 | + return 'var(--color-warning)'; |
| 110 | + default: |
| 111 | + return 'var(--color-base-content)'; |
| 112 | + } |
| 113 | + } |
74 | 114 |
|
75 | 115 | onMount(() => { |
76 | 116 | return () => { |
|
338 | 378 | > |
339 | 379 | <div class="space-y-2"> |
340 | 380 | {#each results as result (result.name)} |
341 | | - <div |
342 | | - class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-[var(--color-base-200)]/50" |
343 | | - > |
344 | | - <StatusPill |
345 | | - variant={statusToVariant(result.status)} |
346 | | - label={t(`health.status.${result.status}`)} |
347 | | - size="sm" |
| 381 | + {@const topErrors = getTopErrors(result)} |
| 382 | + {@const isExpandable = |
| 383 | + topErrors !== null && (result.status === 'warning' || result.status === 'critical')} |
| 384 | + {@const isExpanded = expandedChecks.has(result.name)} |
| 385 | + <div> |
| 386 | + <button |
| 387 | + type="button" |
| 388 | + class="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg bg-[var(--color-base-200)]/50 text-left {isExpandable |
| 389 | + ? 'cursor-pointer hover:bg-[var(--color-base-200)]' |
| 390 | + : 'cursor-default'}" |
| 391 | + onclick={() => isExpandable && toggleExpand(result.name)} |
| 392 | + disabled={!isExpandable} |
| 393 | + aria-expanded={isExpandable ? isExpanded : undefined} |
348 | 394 | > |
349 | | - {#snippet leadingIcon()} |
350 | | - {@render statusIcon(result.status, 'size-3.5')} |
351 | | - {/snippet} |
352 | | - </StatusPill> |
353 | | - <div class="flex-1 min-w-0"> |
354 | | - <span class="text-sm font-medium">{formatCheckName(result.name)}</span> |
355 | | - <p class="text-xs text-[var(--color-base-content)] opacity-60 truncate"> |
356 | | - {result.message} |
357 | | - </p> |
358 | | - </div> |
359 | | - <span class="text-xs text-[var(--color-base-content)] opacity-40 shrink-0"> |
360 | | - {result.duration_ms.toFixed(1)}ms |
361 | | - </span> |
| 395 | + <StatusPill |
| 396 | + variant={statusToVariant(result.status)} |
| 397 | + label={t(`health.status.${result.status}`)} |
| 398 | + size="sm" |
| 399 | + > |
| 400 | + {#snippet leadingIcon()} |
| 401 | + {@render statusIcon(result.status, 'size-3.5')} |
| 402 | + {/snippet} |
| 403 | + </StatusPill> |
| 404 | + <div class="flex-1 min-w-0"> |
| 405 | + <span class="text-sm font-medium">{formatCheckName(result.name)}</span> |
| 406 | + <p class="text-xs text-[var(--color-base-content)] opacity-60 truncate"> |
| 407 | + {result.message} |
| 408 | + </p> |
| 409 | + </div> |
| 410 | + <span class="text-xs text-[var(--color-base-content)] opacity-40 shrink-0"> |
| 411 | + {result.duration_ms.toFixed(1)}ms |
| 412 | + </span> |
| 413 | + {#if isExpandable} |
| 414 | + <ChevronDown |
| 415 | + class="size-4 shrink-0 opacity-40 transition-transform {isExpanded |
| 416 | + ? 'rotate-180' |
| 417 | + : ''}" |
| 418 | + /> |
| 419 | + {/if} |
| 420 | + </button> |
| 421 | + |
| 422 | + {#if isExpanded && topErrors} |
| 423 | + <div |
| 424 | + class="mt-1 ml-3 mr-3 mb-2 rounded-lg bg-[var(--color-base-200)]/30 overflow-hidden" |
| 425 | + > |
| 426 | + <p |
| 427 | + class="px-3 py-1.5 text-xs font-medium text-[var(--color-base-content)] opacity-60" |
| 428 | + > |
| 429 | + {t('health.logs.topErrors')} |
| 430 | + </p> |
| 431 | + <table class="w-full text-xs"> |
| 432 | + <thead> |
| 433 | + <tr |
| 434 | + class="text-left text-[var(--color-base-content)] opacity-50 border-b border-[var(--color-base-200)]" |
| 435 | + > |
| 436 | + <th class="px-3 py-1.5 font-medium">{t('health.logs.errorComponent')}</th> |
| 437 | + <th class="px-3 py-1.5 font-medium">{t('health.logs.errorMessage')}</th> |
| 438 | + <th class="px-3 py-1.5 font-medium text-right" |
| 439 | + >{t('health.logs.errorCount')}</th |
| 440 | + > |
| 441 | + <th class="px-3 py-1.5 font-medium">{t('health.logs.errorLevel')}</th> |
| 442 | + </tr> |
| 443 | + </thead> |
| 444 | + <tbody> |
| 445 | + {#each topErrors as group (group.component + ':' + group.message)} |
| 446 | + <tr class="border-b border-[var(--color-base-200)]/50 last:border-0"> |
| 447 | + <td class="px-3 py-1.5"> |
| 448 | + {#if group.component} |
| 449 | + <span |
| 450 | + class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-base-300)] text-[var(--color-base-content)]" |
| 451 | + > |
| 452 | + {group.component} |
| 453 | + </span> |
| 454 | + {:else} |
| 455 | + <span class="opacity-30">-</span> |
| 456 | + {/if} |
| 457 | + </td> |
| 458 | + <td class="px-3 py-1.5 max-w-[300px] truncate" title={group.message}> |
| 459 | + {group.message} |
| 460 | + </td> |
| 461 | + <td class="px-3 py-1.5 text-right font-mono">{group.count}</td> |
| 462 | + <td class="px-3 py-1.5"> |
| 463 | + <span |
| 464 | + class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium" |
| 465 | + style:color={levelColor(group.level)} |
| 466 | + style:background="color-mix(in srgb, {levelColor(group.level)} 10%, transparent)" |
| 467 | + > |
| 468 | + {group.level} |
| 469 | + </span> |
| 470 | + </td> |
| 471 | + </tr> |
| 472 | + {/each} |
| 473 | + </tbody> |
| 474 | + </table> |
| 475 | + </div> |
| 476 | + {/if} |
362 | 477 | </div> |
363 | 478 | {/each} |
364 | 479 | </div> |
|
0 commit comments