Skip to content

Commit 0c92a4a

Browse files
aksOpsclaude
andauthored
feat(ui): single-view layout with MCP URL in header + full-bleed service map (#76)
UX cleanup: - Removed Traces and Logs views from the UI nav. Service Map is the only remaining view; nav tabs are dropped (single view = no tabs needed). - MCP endpoint URL now lives in the TopNav header with an inline Copy button — accessible from any state, no navigation required. The standalone MCPConsole view is unmounted (file kept temporarily as orphaned source for the next cleanup pass). - Service Map canvas height now scales with viewport via new useWindowHeight hook. Was hard-capped at 660px; now floor=460, cap=windowH-320 so a 1440-tall window gets ~1120px of canvas. - Top-level <Space> in remaining views now overrides display:inline-flex → display:flex with width:100% so the page actually fills wide monitors. The design-system Space defaults to inline-flex which collapses the entire view to its widest child's intrinsic width — the symptom that made even a 34" desktop look mobile-sized. Bundle: 248KB → 233KB after tree-shaking the orphaned views. Verification - make ui-build clean - go build ./... clean - Daemon starts on HTTP_PORT=37776 and serves the new bundle (index-B9ZFj2IV.js) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f64ac4d commit 0c92a4a

11 files changed

Lines changed: 130 additions & 203 deletions

File tree

internal/ui/dist/assets/cytoscape-cose-bilkent-nxdGmLq9.js renamed to internal/ui/dist/assets/cytoscape-cose-bilkent-CEIBo6Gj.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ui/dist/assets/index-B9ZFj2IV.js

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ui/dist/assets/index-Bi057qLa.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

internal/ui/dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>OtelContext</title>
7-
<script type="module" crossorigin src="/assets/index-Bi057qLa.js"></script>
7+
<script type="module" crossorigin src="/assets/index-B9ZFj2IV.js"></script>
88
<link rel="stylesheet" crossorigin href="/assets/index-DzLWOk_K.css">
99
</head>
1010
<body>

ui/src/App.tsx

Lines changed: 11 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,35 @@
1-
import { useCallback, useRef, useState } from 'react'
1+
import { useState } from 'react'
22
import { AppShell } from '@ossrandom/design-system'
33
import TopNav, { type OtelView } from './components/nav/TopNav'
44
import ServicesView from './components/observability/ServicesView'
5-
import TracesView from './components/observability/TracesView'
6-
import LogsView from './components/observability/LogsView'
7-
import MCPConsole from './components/mcp/MCPConsole'
85
import { useSystemGraph } from './hooks/useSystemGraph'
96
import { useDashboard } from './hooks/useDashboard'
10-
import { useTraces } from './hooks/useTraces'
11-
import { useLogs } from './hooks/useLogs'
127
import { useWebSocket } from './hooks/useWebSocket'
13-
import type { LogEntry } from './types/api'
148

159
export default function App() {
1610
const [view, setView] = useState<OtelView>('services')
17-
const [serviceFilter, setServiceFilter] = useState<string | null>(null)
1811

1912
const graph = useSystemGraph()
2013
const dash = useDashboard()
21-
const traces = useTraces()
22-
const logs = useLogs()
2314

24-
const setLogsRef = useRef(logs.setLogs)
25-
setLogsRef.current = logs.setLogs
26-
const appendLogs = useCallback((incoming: LogEntry[]) => {
27-
setLogsRef.current((current) => [...incoming, ...current].slice(0, 200))
28-
}, [])
29-
30-
const ws = useWebSocket(appendLogs)
15+
// WebSocket retained as the live/offline source for the header indicator;
16+
// log batches it pushes are intentionally discarded.
17+
const ws = useWebSocket(() => undefined)
3118
const wsConnected = !!ws.current
3219

33-
const navigateToTraces = useCallback((service: string) => {
34-
setServiceFilter(service)
35-
setView('traces')
36-
}, [])
37-
38-
const navigateToLogs = useCallback((service: string) => {
39-
setServiceFilter(service)
40-
setView('logs')
41-
}, [])
42-
43-
const clearFilter = useCallback(() => {
44-
setServiceFilter(null)
45-
}, [])
46-
4720
return (
4821
<AppShell
4922
header={
5023
<TopNav view={view} onNavigate={setView} wsConnected={wsConnected} />
5124
}
5225
>
53-
{view === 'services' && (
54-
<ServicesView
55-
graph={graph.graph}
56-
loading={graph.loading}
57-
error={graph.error}
58-
dashboard={dash.dashboard}
59-
stats={dash.stats}
60-
onNavigateToTraces={navigateToTraces}
61-
onNavigateToLogs={navigateToLogs}
62-
/>
63-
)}
64-
{view === 'traces' && (
65-
<TracesView
66-
traces={traces.traces}
67-
selected={traces.selected}
68-
loading={traces.loading}
69-
error={traces.error}
70-
onSelect={(traceId: string) => void traces.selectTrace(traceId)}
71-
serviceFilter={serviceFilter}
72-
onClearFilter={clearFilter}
73-
dashboard={dash.dashboard}
74-
/>
75-
)}
76-
{view === 'logs' && (
77-
<LogsView
78-
logs={logs.logs}
79-
similar={logs.similar}
80-
loading={logs.loading}
81-
error={logs.error}
82-
onSimilar={(query: string) => void logs.runSimilar(query)}
83-
serviceFilter={serviceFilter}
84-
onClearFilter={clearFilter}
85-
dashboard={dash.dashboard}
86-
/>
87-
)}
88-
{view === 'mcp' && <MCPConsole />}
26+
<ServicesView
27+
graph={graph.graph}
28+
loading={graph.loading}
29+
error={graph.error}
30+
dashboard={dash.dashboard}
31+
stats={dash.stats}
32+
/>
8933
</AppShell>
9034
)
9135
}

ui/src/components/mcp/MCPConsole.tsx

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Note: MCPConsole is no longer mounted in App.tsx — the MCP endpoint URL
2+
// + Copy button now live in the TopNav header. This file is kept temporarily
3+
// as orphaned source pending a follow-up cleanup pass; it is tree-shaken out
4+
// of the production bundle.
15
import { useState } from 'react'
26
import { Badge, Button, Card, Input, Space } from '@ossrandom/design-system'
37
import { Check, Copy, Terminal } from 'lucide-react'
@@ -13,23 +17,18 @@ export default function MCPConsole() {
1317
}
1418

1519
return (
16-
<Space direction="vertical" size="md">
17-
<Card
18-
bordered
19-
padding="lg"
20-
radius="md"
21-
title={
20+
<Space direction="vertical" size="md" style={{ display: 'flex', width: '100%' }}>
21+
<Card bordered padding="lg" radius="md">
22+
<Space direction="vertical" size="sm" style={{ display: 'flex', width: '100%' }}>
2223
<Space size="xs" align="center">
2324
<Terminal size={14} />
24-
<span>MCP Endpoint</span>
25+
<strong>MCP Endpoint</strong>
26+
<Badge tone="info" size="sm">live</Badge>
2527
</Space>
26-
}
27-
subtitle="Plug any MCP-compatible client (Claude Desktop, Cursor, custom agents) into the URL below."
28-
extra={<Badge tone="info" size="sm">live</Badge>}
29-
>
30-
<Space direction="vertical" size="md">
31-
<Space size="sm" wrap>
32-
<Input value={url} readOnly type="url" />
28+
<Space size="sm" wrap style={{ display: 'flex', width: '100%' }}>
29+
<div style={{ flex: 1, minWidth: 0 }}>
30+
<Input value={url} readOnly type="url" />
31+
</div>
3332
<Button
3433
variant="primary"
3534
size="sm"
@@ -39,12 +38,6 @@ export default function MCPConsole() {
3938
{copied ? 'Copied' : 'Copy'}
4039
</Button>
4140
</Space>
42-
43-
<p>
44-
HTTP Streamable MCP · JSON-RPC 2.0 over POST + Server-Sent Events.
45-
If <code>API_KEY</code> is set on the server, send{' '}
46-
<code>Authorization: Bearer &lt;API_KEY&gt;</code> on every request.
47-
</p>
4841
</Space>
4942
</Card>
5043
</Space>

ui/src/components/nav/TopNav.tsx

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,42 @@
11
import { useState } from 'react'
22
import {
33
Badge,
4-
Drawer,
4+
Button,
55
IconButton,
6-
Menu,
6+
Input,
77
Space,
8-
Tabs,
98
} from '@ossrandom/design-system'
10-
import { Menu as MenuIcon, Moon, Network, Radar, Search, Sun, Terminal } from 'lucide-react'
9+
import { Check, Copy, Moon, Sun } from 'lucide-react'
1110
import { useTheme } from '../../hooks/useTheme'
1211
import { useMediaQuery } from '../../hooks/useMediaQuery'
1312

14-
export type OtelView = 'services' | 'traces' | 'logs' | 'mcp'
13+
// Single-view app: the only "view" is the service map. Kept as a type so the
14+
// AppShell wiring in App.tsx stays open to additional views later.
15+
export type OtelView = 'services'
1516

1617
interface TopNavProps {
1718
view: OtelView
1819
onNavigate: (view: OtelView) => void
1920
wsConnected: boolean
2021
}
2122

22-
const tabs: { key: OtelView; label: string }[] = [
23-
{ key: 'services', label: 'Service Map' },
24-
{ key: 'traces', label: 'Traces' },
25-
{ key: 'logs', label: 'Logs' },
26-
{ key: 'mcp', label: 'MCP' },
27-
]
28-
29-
const menuItems = [
30-
{ key: 'services' as const, label: 'Service Map', icon: <Network size={14} /> },
31-
{ key: 'traces' as const, label: 'Traces', icon: <Search size={14} /> },
32-
{ key: 'logs' as const, label: 'Logs', icon: <Radar size={14} /> },
33-
{ key: 'mcp' as const, label: 'MCP Endpoint', icon: <Terminal size={14} /> },
34-
]
35-
36-
export default function TopNav({ view, onNavigate, wsConnected }: Readonly<TopNavProps>) {
23+
export default function TopNav({ wsConnected }: Readonly<TopNavProps>) {
3724
const { theme, toggle } = useTheme()
3825
const isCompact = useMediaQuery('(max-width: 760px)')
39-
const [drawerOpen, setDrawerOpen] = useState(false)
26+
const [copied, setCopied] = useState(false)
27+
28+
// Resolved at render — works in any deployment because it's whatever the
29+
// browser is already pointed at. Empty during SSR (we run client-only, so
30+
// this is safe).
31+
const mcpUrl =
32+
typeof window !== 'undefined' ? `${window.location.origin}/mcp` : '/mcp'
33+
34+
const copyMcpUrl = async () => {
35+
if (typeof navigator === 'undefined' || !navigator.clipboard) return
36+
await navigator.clipboard.writeText(mcpUrl)
37+
setCopied(true)
38+
window.setTimeout(() => setCopied(false), 1500)
39+
}
4040

4141
const themeBtn = (
4242
<IconButton
@@ -55,59 +55,58 @@ export default function TopNav({ view, onNavigate, wsConnected }: Readonly<TopNa
5555
</Badge>
5656
)
5757

58+
const copyBtn = (
59+
<Button
60+
variant="primary"
61+
size="sm"
62+
iconLeft={copied ? <Check size={12} /> : <Copy size={12} />}
63+
onClick={copyMcpUrl}
64+
>
65+
{copied ? 'Copied' : 'Copy MCP URL'}
66+
</Button>
67+
)
68+
5869
if (isCompact) {
70+
// Compact: brand + copy-only button + indicators. Skip the URL field
71+
// because it eats horizontal real estate that we don't have on phones.
5972
return (
60-
<>
61-
<Space justify="between" align="center" style={{ padding: '0.5rem 0.75rem' }}>
62-
<Space size="sm" align="center">
63-
<IconButton
64-
icon={<MenuIcon size={16} />}
65-
aria-label="Open navigation"
66-
variant="ghost"
67-
size="sm"
68-
onClick={() => setDrawerOpen(true)}
69-
/>
70-
<strong>OtelContext</strong>
71-
</Space>
72-
<Space size="xs" align="center">
73-
{liveBadge}
74-
{themeBtn}
75-
</Space>
73+
<Space
74+
justify="between"
75+
align="center"
76+
style={{ display: 'flex', width: '100%', padding: '0.5rem 0.75rem' }}
77+
>
78+
<strong>OtelContext</strong>
79+
<Space size="xs" align="center">
80+
{copyBtn}
81+
{liveBadge}
82+
{themeBtn}
7683
</Space>
77-
78-
<Drawer
79-
open={drawerOpen}
80-
onClose={() => setDrawerOpen(false)}
81-
placement="left"
82-
width="min(280px, 86vw)"
83-
title="OtelContext"
84-
>
85-
<Menu<OtelView>
86-
mode="vertical"
87-
items={menuItems}
88-
selectedKeys={[view]}
89-
onSelect={(key) => {
90-
onNavigate(key)
91-
setDrawerOpen(false)
92-
}}
93-
/>
94-
</Drawer>
95-
</>
84+
</Space>
9685
)
9786
}
9887

88+
// Desktop: brand on the left, MCP URL field grows to fill, copy button +
89+
// status indicators sit on the right. Single-row, no tabs (only one view).
9990
return (
100-
<Space justify="between" align="center" style={{ padding: '0.4rem 1rem' }}>
91+
<Space
92+
justify="between"
93+
align="center"
94+
style={{ display: 'flex', width: '100%', padding: '0.5rem 1rem', gap: '0.75rem' }}
95+
>
10196
<Space size="md" align="center">
10297
<strong>OtelContext</strong>
103-
<Tabs<OtelView>
104-
items={tabs}
105-
value={view}
106-
variant="line"
107-
onChange={(key) => onNavigate(key)}
108-
/>
10998
</Space>
99+
<div style={{ flex: 1, minWidth: 0, maxWidth: 640 }}>
100+
<Input
101+
value={mcpUrl}
102+
readOnly
103+
type="url"
104+
size="sm"
105+
aria-label="MCP endpoint URL"
106+
/>
107+
</div>
110108
<Space size="sm" align="center">
109+
{copyBtn}
111110
{liveBadge}
112111
{themeBtn}
113112
</Space>

ui/src/components/observability/ServiceSidePanel.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import React from 'react'
22
import { Alert, Badge, Button, Card, Grid, IconButton, Progress, Space, Stat } from '@ossrandom/design-system'
3-
import { ArrowRight, X } from 'lucide-react'
3+
import { X } from 'lucide-react'
44
import type { SystemNode, SystemEdge } from '../../types/api'
55

66
interface ServiceSidePanelProps {
77
node: SystemNode
88
edges: SystemEdge[]
99
onClose: () => void
1010
onSelectService: (id: string) => void
11-
onViewTraces: (service: string) => void
12-
onViewLogs: (service: string) => void
1311
}
1412

1513
function statusTone(status: string): 'info' | 'warning' | 'danger' | 'neutral' {
@@ -24,8 +22,6 @@ const ServiceSidePanel: React.FC<ServiceSidePanelProps> = ({
2422
edges,
2523
onClose,
2624
onSelectService,
27-
onViewTraces,
28-
onViewLogs,
2925
}) => {
3026
const upstream = edges.filter((e) => e.target === node.id)
3127
const downstream = edges.filter((e) => e.source === node.id)
@@ -126,26 +122,6 @@ const ServiceSidePanel: React.FC<ServiceSidePanelProps> = ({
126122
</Space>
127123
)}
128124

129-
<Space size="xs">
130-
<Button
131-
variant="secondary"
132-
size="sm"
133-
block
134-
iconRight={<ArrowRight size={11} />}
135-
onClick={() => onViewTraces(node.id)}
136-
>
137-
Traces
138-
</Button>
139-
<Button
140-
variant="secondary"
141-
size="sm"
142-
block
143-
iconRight={<ArrowRight size={11} />}
144-
onClick={() => onViewLogs(node.id)}
145-
>
146-
Logs
147-
</Button>
148-
</Space>
149125
</Space>
150126
)
151127
}

0 commit comments

Comments
 (0)