From 2bf184b12b907c1465dc6a10eceb2ef19594c3f1 Mon Sep 17 00:00:00 2001 From: dlwldn30 Date: Sat, 29 Aug 2026 22:53:14 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=94=A8=20Trace=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=20=EA=B8=B0=EB=A1=9D=20=ED=8C=A8=EB=84=90=EA=B3=BC=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=ED=9D=90=EB=A6=84=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/e2e/distributed-trace.spec.ts | 19 ++-- .../workbench/views/TraceHistoryPanel.tsx | 22 ++++- .../features/workbench/views/TraceView.css | 87 ++++++++----------- .../features/workbench/views/TraceView.tsx | 28 ++---- .../workbench/views/traceWorkspace.test.tsx | 20 ++++- 5 files changed, 94 insertions(+), 82 deletions(-) diff --git a/frontend/e2e/distributed-trace.spec.ts b/frontend/e2e/distributed-trace.spec.ts index f1327ad..1157f73 100644 --- a/frontend/e2e/distributed-trace.spec.ts +++ b/frontend/e2e/distributed-trace.spec.ts @@ -24,6 +24,10 @@ test.describe.serial('분산 Trace 사용자 흐름', () => { await expectMetric(outcome, '진입 서비스', 'order-service') await expect(outcome.getByText(/2개 · 경계 1회/)).toBeVisible() + const history = page.getByRole('region', { name: '최근 Trace 탐색' }) + await expect(history).toBeVisible() + await expect(history.locator('.trace-item.is-selected')).toHaveAccessibleName(/\/lab\/orders\/2001 · 성공/) + const waterfall = page.getByRole('region', { name: 'Span 타임라인' }) await expect(waterfall.getByText('서비스 경계', { exact: true })).toBeVisible() await expect(waterfall.getByText('order-service → product-service', { exact: true })).toBeVisible() @@ -32,6 +36,16 @@ test.describe.serial('분산 Trace 사용자 흐름', () => { await expect(page.locator('.service-area').filter({ hasText: 'order-service' })).toBeVisible() await expect(page.locator('.service-area').filter({ hasText: 'product-service' })).toBeVisible() await expect(page.locator('.react-flow__edge-text').filter({ hasText: 'order-service → product-service' })).toBeVisible() + + await page.setViewportSize({ width: 390, height: 844 }) + await expect.poll(() => page.evaluate(() => + document.documentElement.scrollWidth - document.documentElement.clientWidth, + )).toBe(0) + const graphTop = await page.locator('.graph-panel').evaluate((element) => element.getBoundingClientRect().top + window.scrollY) + const inspectorTop = await page.locator('.workspace--runtime .right-panel').evaluate((element) => element.getBoundingClientRect().top + window.scrollY) + const historyTop = await page.getByRole('region', { name: '최근 Trace 탐색' }).evaluate((element) => element.getBoundingClientRect().top + window.scrollY) + expect(graphTop).toBeLessThan(inspectorTop) + expect(inspectorTop).toBeLessThan(historyTop) }) test('Product PostgreSQL timeout의 원인과 Order까지의 오류 전파를 표시한다', async ({ page }) => { @@ -53,11 +67,6 @@ test.describe.serial('분산 Trace 사용자 흐름', () => { await expect(inspector).toContainText(/PostgreSQL|POSTGRESQL/) await expect(inspector.locator('.trace-inspector-error')).toBeVisible() - await page.setViewportSize({ width: 390, height: 844 }) - await expect.poll(() => page.evaluate(() => - document.documentElement.scrollWidth - document.documentElement.clientWidth, - )).toBe(0) - await expect(page.getByRole('region', { name: 'Span 타임라인' })).toBeVisible() }) }) diff --git a/frontend/src/features/workbench/views/TraceHistoryPanel.tsx b/frontend/src/features/workbench/views/TraceHistoryPanel.tsx index 422bb72..d815f14 100644 --- a/frontend/src/features/workbench/views/TraceHistoryPanel.tsx +++ b/frontend/src/features/workbench/views/TraceHistoryPanel.tsx @@ -1,3 +1,4 @@ +import { ArrowRight } from 'lucide-react' import type { TraceSummary } from '../../../types/trace' import { EVENT_STATUS_LABEL } from '../../../ui/copy' import type { TraceHistoryFilter } from '../traceModel' @@ -9,6 +10,7 @@ interface TraceHistoryPanelProps { selectedTraceId: string | null onFilterChange: (filter: TraceHistoryFilter) => void onSelectTrace: (traceId: string) => void + onNavigateToRequest: () => void } const FILTERS: { value: TraceHistoryFilter; label: string }[] = [ @@ -25,9 +27,10 @@ export function TraceHistoryPanel({ selectedTraceId, onFilterChange, onSelectTrace, + onNavigateToRequest, }: TraceHistoryPanelProps) { return ( -
+
실행 기록 @@ -52,17 +55,28 @@ export function TraceHistoryPanel({
{traces.length === 0 ? ( -

{totalCount === 0 ? '아직 수집된 Trace가 없습니다.' : '이 조건에 맞는 Trace가 없습니다.'}

+
+

{totalCount === 0 ? '아직 수집된 Trace가 없습니다.' : '이 조건에 맞는 Trace가 없습니다.'}

+ {totalCount === 0 ? ( + + ) : null} +
) : ( traces.map((trace) => { const collectionTimedOut = trace.traceCollectionStatus === 'TIMED_OUT' const tone = collectionTimedOut ? 'collection-timeout' : trace.resultStatus.toLowerCase() + const statusLabel = collectionTimedOut ? '수집 시간 초과' : EVENT_STATUS_LABEL[trace.resultStatus] return (
-
+
) } diff --git a/frontend/src/features/workbench/views/TraceView.css b/frontend/src/features/workbench/views/TraceView.css index 1bec0a0..7a21cd3 100644 --- a/frontend/src/features/workbench/views/TraceView.css +++ b/frontend/src/features/workbench/views/TraceView.css @@ -1,6 +1,30 @@ .trace-history-panel { min-width: 0; } + +.trace-history-empty { + display: grid; + justify-items: start; + gap: 10px; +} + +.trace-history-empty p { + margin: 0; +} + +.trace-history-empty button { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid rgba(37, 99, 235, 0.28); + border-radius: 5px; + background: var(--color-surface); + color: var(--color-action); + padding: 7px 9px; + font-size: 0.68rem; + font-weight: 750; + cursor: pointer; +} .trace-history-filters { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -452,35 +476,6 @@ } } -@media (max-width: 1220px) { - .workspace--runtime .right-panel { - grid-column: 2; - grid-template-columns: 1fr; - } -} - -@media (min-width: 1221px) and (max-width: 1320px) { - .workspace--runtime .right-panel { - grid-column: 2; - grid-template-columns: 1fr; - } - - .workspace--runtime .trace-inspector { - border-right: 0; - } -} - -@media (max-width: 980px) { - .workspace--runtime .right-panel { - grid-column: 1; - grid-row: 3; - } - - .workspace--runtime .left-panel { - grid-row: 4; - } -} - @media (max-width: 720px) { .workspace--runtime, .workspace--runtime > *, @@ -554,8 +549,7 @@ } .workspace--project .recent-card, -.workspace--api .recent-card, -.workspace--runtime .control-card { +.workspace--api .recent-card { display: none; } @@ -1287,35 +1281,23 @@ @media (min-width: 1221px) and (max-width: 1320px) { .workspace--runtime { - grid-template-columns: 280px minmax(0, 1fr); + grid-template-columns: 240px minmax(0, 1fr) 320px; + } +} + +@media (max-width: 1220px) { +.workspace--runtime { + grid-template-columns: 330px minmax(0, 1fr); } .workspace--runtime .right-panel { position: static; grid-column: 1 / -1; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0; max-height: none; border-top: 1px solid var(--color-line); border-left: 0; overflow: visible; } - -.workspace--runtime .inspector-workbench, -.workspace--runtime .timeline-card { - border-right: 1px solid var(--color-line); - } -} - -@media (max-width: 1220px) { -.workspace--runtime { - grid-template-columns: 330px minmax(0, 1fr); - } - -.timeline-card { - border-right: 1px solid var(--color-line); - } } @media (max-width: 980px) { @@ -1324,11 +1306,12 @@ } .workspace--runtime .left-panel { - grid-row: 3; + grid-row: 4; } .workspace--runtime .right-panel { - grid-row: 4; + grid-column: 1; + grid-row: 3; } } diff --git a/frontend/src/features/workbench/views/TraceView.tsx b/frontend/src/features/workbench/views/TraceView.tsx index 7955f6e..07b8b78 100644 --- a/frontend/src/features/workbench/views/TraceView.tsx +++ b/frontend/src/features/workbench/views/TraceView.tsx @@ -29,25 +29,15 @@ export function TraceView({ model }: TraceViewProps) { return (
diff --git a/frontend/src/features/workbench/views/traceWorkspace.test.tsx b/frontend/src/features/workbench/views/traceWorkspace.test.tsx index a5f48c9..80f1605 100644 --- a/frontend/src/features/workbench/views/traceWorkspace.test.tsx +++ b/frontend/src/features/workbench/views/traceWorkspace.test.tsx @@ -64,7 +64,7 @@ describe('Trace workspace presentation', () => { httpStatus: 200, durationMs: 12, startedAt: new Date(0).toISOString(), traceCollectionStatus: 'DISABLED', }] render( - , + , ) fireEvent.click(screen.getByRole('button', { name: '확인 필요' })) expect(onFilterChange).toHaveBeenCalledWith('attention') @@ -76,12 +76,28 @@ describe('Trace workspace presentation', () => { httpStatus: 200, durationMs: 12, startedAt: new Date(0).toISOString(), traceCollectionStatus: 'TIMED_OUT', }] const { container } = render( - , + , ) const timeoutPill = within(container).getByText('수집 시간 초과') expect(timeoutPill).toHaveClass('pill--warning') }) + it('offers API navigation only when there is no trace history at all', () => { + const onNavigateToRequest = vi.fn() + const { rerender } = render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'API 요청으로 이동' })) + expect(onNavigateToRequest).toHaveBeenCalledOnce() + + rerender( + , + ) + expect(screen.getByText('이 조건에 맞는 Trace가 없습니다.')).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'API 요청으로 이동' })).not.toBeInTheDocument() + }) + it('shows selected span facts and keeps raw details collapsed', () => { const selectedNode: GraphNodeState = { id: 'postgres', component: 'POSTGRESQL', label: 'SELECT product', status: 'TIMEOUT',