Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions frontend/e2e/distributed-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 }) => {
Expand All @@ -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()
})
})

Expand Down
22 changes: 18 additions & 4 deletions frontend/src/features/workbench/views/TraceHistoryPanel.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 }[] = [
Expand All @@ -25,9 +27,10 @@ export function TraceHistoryPanel({
selectedTraceId,
onFilterChange,
onSelectTrace,
onNavigateToRequest,
}: TraceHistoryPanelProps) {
return (
<div className="panel-card recent-card trace-history-panel">
<section className="panel-card recent-card trace-history-panel" aria-label="์ตœ๊ทผ Trace ํƒ์ƒ‰">
<div className="panel-header">
<div>
<span className="section-label">์‹คํ–‰ ๊ธฐ๋ก</span>
Expand All @@ -52,25 +55,36 @@ export function TraceHistoryPanel({

<div className="trace-list">
{traces.length === 0 ? (
<p className="empty-copy">{totalCount === 0 ? '์•„์ง ์ˆ˜์ง‘๋œ Trace๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' : '์ด ์กฐ๊ฑด์— ๋งž๋Š” Trace๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'}</p>
<div className="trace-history-empty">
<p className="empty-copy">{totalCount === 0 ? '์•„์ง ์ˆ˜์ง‘๋œ Trace๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' : '์ด ์กฐ๊ฑด์— ๋งž๋Š” Trace๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'}</p>
{totalCount === 0 ? (
<button type="button" onClick={onNavigateToRequest}>
API ์š”์ฒญ์œผ๋กœ ์ด๋™
<ArrowRight size={15} aria-hidden="true" />
</button>
) : null}
</div>
) : (
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 (
<button
key={trace.traceId}
type="button"
className={`trace-item trace-item--${tone}${selectedTraceId === trace.traceId ? ' is-selected' : ''}`}
onClick={() => onSelectTrace(trace.traceId)}
title={trace.endpoint}
aria-label={`${trace.endpoint} ยท ${statusLabel} ยท Trace ${trace.traceId.slice(0, 8)}`}
>
<div>
<strong>{trace.endpoint}</strong>
<span>{trace.traceId.slice(0, 8)}</span>
</div>
<div>
<span className={`pill pill--inline pill--${collectionTimedOut ? 'warning' : tone}`}>
{collectionTimedOut ? '์ˆ˜์ง‘ ์‹œ๊ฐ„ ์ดˆ๊ณผ' : EVENT_STATUS_LABEL[trace.resultStatus]}
{statusLabel}
</span>
<span>{trace.durationMs}ms</span>
</div>
Expand All @@ -79,6 +93,6 @@ export function TraceHistoryPanel({
})
)}
</div>
</div>
</section>
)
}
87 changes: 35 additions & 52 deletions frontend/src/features/workbench/views/TraceView.css
Original file line number Diff line number Diff line change
@@ -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));
Expand Down Expand Up @@ -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 > *,
Expand Down Expand Up @@ -554,8 +549,7 @@
}

.workspace--project .recent-card,
.workspace--api .recent-card,
.workspace--runtime .control-card {
.workspace--api .recent-card {
display: none;
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
}

Expand Down
28 changes: 9 additions & 19 deletions frontend/src/features/workbench/views/TraceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,15 @@ export function TraceView({ model }: TraceViewProps) {
return (
<section className="workspace workspace--runtime">
<aside className="left-panel control-rail">
<div className="panel-card control-card">
<div className="panel-header control-header">
<div>
<span className="section-label">์‹คํ–‰ ๊ธฐ๋ก</span>
<h2>์ตœ๊ทผ Trace</h2>
<p>์ด์ „ ์‹คํ–‰ ๊ธฐ๋ก์„ ๋‹ค์‹œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
</div>
<StatusBadge tone="info">{recentTraces.length}๊ฐœ ๊ธฐ๋ก</StatusBadge>
</div>
<TraceHistoryPanel
traces={filteredRecentTraces}
totalCount={recentTraces.length}
filter={traceHistoryFilter}
selectedTraceId={traceDetail?.traceId ?? null}
onFilterChange={setTraceHistoryFilter}
onSelectTrace={(traceId) => void selectTrace(traceId)}
/>

</div>
<TraceHistoryPanel
traces={filteredRecentTraces}
totalCount={recentTraces.length}
filter={traceHistoryFilter}
selectedTraceId={traceDetail?.traceId ?? null}
onFilterChange={setTraceHistoryFilter}
onSelectTrace={(traceId) => void selectTrace(traceId)}
onNavigateToRequest={() => setActiveView('api')}
/>
</aside>
<section className="graph-panel">
<div className="panel-card panel-card--graph">
Expand Down
20 changes: 18 additions & 2 deletions frontend/src/features/workbench/views/traceWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Trace workspace presentation', () => {
httpStatus: 200, durationMs: 12, startedAt: new Date(0).toISOString(), traceCollectionStatus: 'DISABLED',
}]
render(
<TraceHistoryPanel traces={traces} totalCount={1} filter="all" selectedTraceId={null} onFilterChange={onFilterChange} onSelectTrace={vi.fn()} />,
<TraceHistoryPanel traces={traces} totalCount={1} filter="all" selectedTraceId={null} onFilterChange={onFilterChange} onSelectTrace={vi.fn()} onNavigateToRequest={vi.fn()} />,
)
fireEvent.click(screen.getByRole('button', { name: 'ํ™•์ธ ํ•„์š”' }))
expect(onFilterChange).toHaveBeenCalledWith('attention')
Expand All @@ -76,12 +76,28 @@ describe('Trace workspace presentation', () => {
httpStatus: 200, durationMs: 12, startedAt: new Date(0).toISOString(), traceCollectionStatus: 'TIMED_OUT',
}]
const { container } = render(
<TraceHistoryPanel traces={traces} totalCount={1} filter="all" selectedTraceId={null} onFilterChange={vi.fn()} onSelectTrace={vi.fn()} />,
<TraceHistoryPanel traces={traces} totalCount={1} filter="all" selectedTraceId={null} onFilterChange={vi.fn()} onSelectTrace={vi.fn()} onNavigateToRequest={vi.fn()} />,
)
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(
<TraceHistoryPanel traces={[]} totalCount={0} filter="all" selectedTraceId={null} onFilterChange={vi.fn()} onSelectTrace={vi.fn()} onNavigateToRequest={onNavigateToRequest} />,
)

fireEvent.click(screen.getByRole('button', { name: 'API ์š”์ฒญ์œผ๋กœ ์ด๋™' }))
expect(onNavigateToRequest).toHaveBeenCalledOnce()

rerender(
<TraceHistoryPanel traces={[]} totalCount={2} filter="timeout" selectedTraceId={null} onFilterChange={vi.fn()} onSelectTrace={vi.fn()} onNavigateToRequest={onNavigateToRequest} />,
)
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',
Expand Down
Loading