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

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions internal/ui/dist/assets/index-B9ZFj2IV.js

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions internal/ui/dist/assets/index-Bi057qLa.js

This file was deleted.

2 changes: 1 addition & 1 deletion internal/ui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OtelContext</title>
<script type="module" crossorigin src="/assets/index-Bi057qLa.js"></script>
<script type="module" crossorigin src="/assets/index-B9ZFj2IV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DzLWOk_K.css">
</head>
<body>
Expand Down
78 changes: 11 additions & 67 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,35 @@
import { useCallback, useRef, useState } from 'react'
import { useState } from 'react'
import { AppShell } from '@ossrandom/design-system'
import TopNav, { type OtelView } from './components/nav/TopNav'
import ServicesView from './components/observability/ServicesView'
import TracesView from './components/observability/TracesView'
import LogsView from './components/observability/LogsView'
import MCPConsole from './components/mcp/MCPConsole'
import { useSystemGraph } from './hooks/useSystemGraph'
import { useDashboard } from './hooks/useDashboard'
import { useTraces } from './hooks/useTraces'
import { useLogs } from './hooks/useLogs'
import { useWebSocket } from './hooks/useWebSocket'
import type { LogEntry } from './types/api'

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

const graph = useSystemGraph()
const dash = useDashboard()
const traces = useTraces()
const logs = useLogs()

const setLogsRef = useRef(logs.setLogs)
setLogsRef.current = logs.setLogs
const appendLogs = useCallback((incoming: LogEntry[]) => {
setLogsRef.current((current) => [...incoming, ...current].slice(0, 200))
}, [])

const ws = useWebSocket(appendLogs)
// WebSocket retained as the live/offline source for the header indicator;
// log batches it pushes are intentionally discarded.
const ws = useWebSocket(() => undefined)
const wsConnected = !!ws.current

const navigateToTraces = useCallback((service: string) => {
setServiceFilter(service)
setView('traces')
}, [])

const navigateToLogs = useCallback((service: string) => {
setServiceFilter(service)
setView('logs')
}, [])

const clearFilter = useCallback(() => {
setServiceFilter(null)
}, [])

return (
<AppShell
header={
<TopNav view={view} onNavigate={setView} wsConnected={wsConnected} />
}
>
{view === 'services' && (
<ServicesView
graph={graph.graph}
loading={graph.loading}
error={graph.error}
dashboard={dash.dashboard}
stats={dash.stats}
onNavigateToTraces={navigateToTraces}
onNavigateToLogs={navigateToLogs}
/>
)}
{view === 'traces' && (
<TracesView
traces={traces.traces}
selected={traces.selected}
loading={traces.loading}
error={traces.error}
onSelect={(traceId: string) => void traces.selectTrace(traceId)}
serviceFilter={serviceFilter}
onClearFilter={clearFilter}
dashboard={dash.dashboard}
/>
)}
{view === 'logs' && (
<LogsView
logs={logs.logs}
similar={logs.similar}
loading={logs.loading}
error={logs.error}
onSimilar={(query: string) => void logs.runSimilar(query)}
serviceFilter={serviceFilter}
onClearFilter={clearFilter}
dashboard={dash.dashboard}
/>
)}
{view === 'mcp' && <MCPConsole />}
<ServicesView
graph={graph.graph}
loading={graph.loading}
error={graph.error}
dashboard={dash.dashboard}
stats={dash.stats}
/>
</AppShell>
)
}
33 changes: 13 additions & 20 deletions ui/src/components/mcp/MCPConsole.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Note: MCPConsole is no longer mounted in App.tsx — the MCP endpoint URL
// + Copy button now live in the TopNav header. This file is kept temporarily
// as orphaned source pending a follow-up cleanup pass; it is tree-shaken out
// of the production bundle.
import { useState } from 'react'
import { Badge, Button, Card, Input, Space } from '@ossrandom/design-system'
import { Check, Copy, Terminal } from 'lucide-react'
Expand All @@ -13,23 +17,18 @@ export default function MCPConsole() {
}

return (
<Space direction="vertical" size="md">
<Card
bordered
padding="lg"
radius="md"
title={
<Space direction="vertical" size="md" style={{ display: 'flex', width: '100%' }}>
<Card bordered padding="lg" radius="md">
<Space direction="vertical" size="sm" style={{ display: 'flex', width: '100%' }}>
<Space size="xs" align="center">
<Terminal size={14} />
<span>MCP Endpoint</span>
<strong>MCP Endpoint</strong>
<Badge tone="info" size="sm">live</Badge>
</Space>
}
subtitle="Plug any MCP-compatible client (Claude Desktop, Cursor, custom agents) into the URL below."
extra={<Badge tone="info" size="sm">live</Badge>}
>
<Space direction="vertical" size="md">
<Space size="sm" wrap>
<Input value={url} readOnly type="url" />
<Space size="sm" wrap style={{ display: 'flex', width: '100%' }}>
<div style={{ flex: 1, minWidth: 0 }}>
<Input value={url} readOnly type="url" />
</div>
<Button
variant="primary"
size="sm"
Expand All @@ -39,12 +38,6 @@ export default function MCPConsole() {
{copied ? 'Copied' : 'Copy'}
</Button>
</Space>

<p>
HTTP Streamable MCP · JSON-RPC 2.0 over POST + Server-Sent Events.
If <code>API_KEY</code> is set on the server, send{' '}
<code>Authorization: Bearer &lt;API_KEY&gt;</code> on every request.
</p>
</Space>
</Card>
</Space>
Expand Down
125 changes: 62 additions & 63 deletions ui/src/components/nav/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { useState } from 'react'
import {
Badge,
Drawer,
Button,
IconButton,
Menu,
Input,
Space,
Tabs,
} from '@ossrandom/design-system'
import { Menu as MenuIcon, Moon, Network, Radar, Search, Sun, Terminal } from 'lucide-react'
import { Check, Copy, Moon, Sun } from 'lucide-react'
import { useTheme } from '../../hooks/useTheme'
import { useMediaQuery } from '../../hooks/useMediaQuery'

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

interface TopNavProps {
view: OtelView
onNavigate: (view: OtelView) => void
wsConnected: boolean
}

const tabs: { key: OtelView; label: string }[] = [
{ key: 'services', label: 'Service Map' },
{ key: 'traces', label: 'Traces' },
{ key: 'logs', label: 'Logs' },
{ key: 'mcp', label: 'MCP' },
]

const menuItems = [
{ key: 'services' as const, label: 'Service Map', icon: <Network size={14} /> },
{ key: 'traces' as const, label: 'Traces', icon: <Search size={14} /> },
{ key: 'logs' as const, label: 'Logs', icon: <Radar size={14} /> },
{ key: 'mcp' as const, label: 'MCP Endpoint', icon: <Terminal size={14} /> },
]

export default function TopNav({ view, onNavigate, wsConnected }: Readonly<TopNavProps>) {
export default function TopNav({ wsConnected }: Readonly<TopNavProps>) {
const { theme, toggle } = useTheme()
const isCompact = useMediaQuery('(max-width: 760px)')
const [drawerOpen, setDrawerOpen] = useState(false)
const [copied, setCopied] = useState(false)

// Resolved at render — works in any deployment because it's whatever the
// browser is already pointed at. Empty during SSR (we run client-only, so
// this is safe).
const mcpUrl =
typeof window !== 'undefined' ? `${window.location.origin}/mcp` : '/mcp'

Check warning on line 32 in ui/src/components/nav/TopNav.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_otelcontext&issues=AZ3uuaXUi61XdRxEIoUt&open=AZ3uuaXUi61XdRxEIoUt&pullRequest=76

Check warning on line 32 in ui/src/components/nav/TopNav.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_otelcontext&issues=AZ3uuaXUi61XdRxEIoUu&open=AZ3uuaXUi61XdRxEIoUu&pullRequest=76

Check warning on line 32 in ui/src/components/nav/TopNav.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_otelcontext&issues=AZ3uuaXUi61XdRxEIoUs&open=AZ3uuaXUi61XdRxEIoUs&pullRequest=76

const copyMcpUrl = async () => {
if (typeof navigator === 'undefined' || !navigator.clipboard) return
await navigator.clipboard.writeText(mcpUrl)
setCopied(true)
window.setTimeout(() => setCopied(false), 1500)

Check warning on line 38 in ui/src/components/nav/TopNav.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_otelcontext&issues=AZ3uuaXUi61XdRxEIoUv&open=AZ3uuaXUi61XdRxEIoUv&pullRequest=76
}

const themeBtn = (
<IconButton
Expand All @@ -55,59 +55,58 @@
</Badge>
)

const copyBtn = (
<Button
variant="primary"
size="sm"
iconLeft={copied ? <Check size={12} /> : <Copy size={12} />}
onClick={copyMcpUrl}
>
{copied ? 'Copied' : 'Copy MCP URL'}
</Button>
)

if (isCompact) {
// Compact: brand + copy-only button + indicators. Skip the URL field
// because it eats horizontal real estate that we don't have on phones.
return (
<>
<Space justify="between" align="center" style={{ padding: '0.5rem 0.75rem' }}>
<Space size="sm" align="center">
<IconButton
icon={<MenuIcon size={16} />}
aria-label="Open navigation"
variant="ghost"
size="sm"
onClick={() => setDrawerOpen(true)}
/>
<strong>OtelContext</strong>
</Space>
<Space size="xs" align="center">
{liveBadge}
{themeBtn}
</Space>
<Space
justify="between"
align="center"
style={{ display: 'flex', width: '100%', padding: '0.5rem 0.75rem' }}
>
<strong>OtelContext</strong>
<Space size="xs" align="center">
{copyBtn}
{liveBadge}
{themeBtn}
</Space>

<Drawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
placement="left"
width="min(280px, 86vw)"
title="OtelContext"
>
<Menu<OtelView>
mode="vertical"
items={menuItems}
selectedKeys={[view]}
onSelect={(key) => {
onNavigate(key)
setDrawerOpen(false)
}}
/>
</Drawer>
</>
</Space>
)
}

// Desktop: brand on the left, MCP URL field grows to fill, copy button +
// status indicators sit on the right. Single-row, no tabs (only one view).
return (
<Space justify="between" align="center" style={{ padding: '0.4rem 1rem' }}>
<Space
justify="between"
align="center"
style={{ display: 'flex', width: '100%', padding: '0.5rem 1rem', gap: '0.75rem' }}
>
<Space size="md" align="center">
<strong>OtelContext</strong>
<Tabs<OtelView>
items={tabs}
value={view}
variant="line"
onChange={(key) => onNavigate(key)}
/>
</Space>
<div style={{ flex: 1, minWidth: 0, maxWidth: 640 }}>
<Input
value={mcpUrl}
readOnly
type="url"
size="sm"
aria-label="MCP endpoint URL"
/>
</div>
<Space size="sm" align="center">
{copyBtn}
{liveBadge}
{themeBtn}
</Space>
Expand Down
26 changes: 1 addition & 25 deletions ui/src/components/observability/ServiceSidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React from 'react'
import { Alert, Badge, Button, Card, Grid, IconButton, Progress, Space, Stat } from '@ossrandom/design-system'
import { ArrowRight, X } from 'lucide-react'
import { X } from 'lucide-react'
import type { SystemNode, SystemEdge } from '../../types/api'

interface ServiceSidePanelProps {
node: SystemNode
edges: SystemEdge[]
onClose: () => void
onSelectService: (id: string) => void
onViewTraces: (service: string) => void
onViewLogs: (service: string) => void
}

function statusTone(status: string): 'info' | 'warning' | 'danger' | 'neutral' {
Expand All @@ -24,8 +22,6 @@ const ServiceSidePanel: React.FC<ServiceSidePanelProps> = ({
edges,
onClose,
onSelectService,
onViewTraces,
onViewLogs,
}) => {
const upstream = edges.filter((e) => e.target === node.id)
const downstream = edges.filter((e) => e.source === node.id)
Expand Down Expand Up @@ -126,26 +122,6 @@ const ServiceSidePanel: React.FC<ServiceSidePanelProps> = ({
</Space>
)}

<Space size="xs">
<Button
variant="secondary"
size="sm"
block
iconRight={<ArrowRight size={11} />}
onClick={() => onViewTraces(node.id)}
>
Traces
</Button>
<Button
variant="secondary"
size="sm"
block
iconRight={<ArrowRight size={11} />}
onClick={() => onViewLogs(node.id)}
>
Logs
</Button>
</Space>
</Space>
)
}
Expand Down
Loading
Loading