Files
cloudlysis/control/ui/src/app/layout.tsx
Vlad Durnea 1298d9a3df
Some checks failed
ci / rust (push) Failing after 2m34s
ci / ui (push) Failing after 30s
Monorepo consolidation: workspace, shared types, transport plans, docker/swam assets
2026-03-30 11:40:42 +03:00

184 lines
5.9 KiB
TypeScript

import { useMemo, useState } from 'react'
import { Link, Outlet, useLocation } from 'react-router-dom'
import { getLastRequestIds } from '../api/client'
import { Button, Code, TextInput } from '../components/primitives'
type NavItem = {
label: string
to: string
}
const navItems: NavItem[] = [
{ label: 'Overview', to: '/' },
{ label: 'Tenants', to: '/tenants' },
{ label: 'Users', to: '/users' },
{ label: 'Sessions', to: '/sessions' },
{ label: 'Roles & Permissions', to: '/roles-permissions' },
{ label: 'Config', to: '/config' },
{ label: 'Definitions', to: '/definitions' },
{ label: 'Scale & Placement', to: '/scale-placement' },
{ label: 'Deployments', to: '/deployments' },
{ label: 'Observability', to: '/observability' },
{ label: 'Audit Log', to: '/audit-log' },
{ label: 'Settings', to: '/settings' },
]
function normalizePath(pathname: string) {
if (pathname === '') return '/'
if (pathname === '/') return '/'
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
}
export function Layout() {
const location = useLocation()
const active = normalizePath(location.pathname)
const [query, setQuery] = useState('')
const lastIds = getLastRequestIds()
const grafana = useMemo(() => {
const base = (import.meta.env.VITE_GRAFANA_URL as string | undefined) ?? ''
const loki = (import.meta.env.VITE_GRAFANA_LOKI_DATASOURCE as string | undefined) ?? 'Loki'
const tempo = (import.meta.env.VITE_GRAFANA_TEMPO_DATASOURCE as string | undefined) ?? 'Tempo'
return { base, loki, tempo }
}, [])
function openGrafanaLogs(id: string) {
if (!grafana.base) return
const left = encodeURIComponent(
JSON.stringify({
datasource: grafana.loki,
queries: [{ refId: 'A', expr: `{correlation_id="${id}"}` }],
}),
)
window.open(`${grafana.base.replace(/\/$/, '')}/explore?left=${left}`, '_blank', 'noreferrer')
}
function openGrafanaTrace(id: string) {
if (!grafana.base) return
const left = encodeURIComponent(
JSON.stringify({
datasource: grafana.tempo,
queries: [{ refId: 'A', queryType: 'traceId', traceId: id }],
}),
)
window.open(`${grafana.base.replace(/\/$/, '')}/explore?left=${left}`, '_blank', 'noreferrer')
}
async function copy(text: string) {
try {
await navigator.clipboard.writeText(text)
} catch {
return
}
}
return (
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}>
<aside
style={{
width: 260,
borderRight: '1px solid #eee',
padding: 16,
background: '#fafafa',
}}
>
<div style={{ fontWeight: 700, marginBottom: 16 }}>Cloudlysis Control</div>
<nav style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{navItems.map((item) => {
const isActive = active === normalizePath(item.to)
return (
<Link
key={item.to}
to={item.to}
style={{
textDecoration: 'none',
color: '#111',
padding: '6px 10px',
borderRadius: 8,
background: isActive ? '#eaeaea' : 'transparent',
}}
>
{item.label}
</Link>
)
})}
</nav>
</aside>
<div style={{ flex: 1 }}>
<header
style={{
borderBottom: '1px solid #eee',
padding: 16,
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<div style={{ width: 420, maxWidth: '100%' }}>
<TextInput
ariaLabel="Global search"
placeholder="Search request/correlation/trace id"
value={query}
onChange={setQuery}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const id = query.trim()
if (!id) return
openGrafanaLogs(id)
}
}}
/>
</div>
<Button
onClick={() => {
const id = query.trim()
if (!id) return
openGrafanaLogs(id)
}}
disabled={!grafana.base}
>
Logs
</Button>
<Button
onClick={() => {
const id = query.trim()
if (!id) return
openGrafanaTrace(id)
}}
disabled={!grafana.base}
>
Trace
</Button>
</div>
{lastIds ? (
<div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>request_id</span>
<Code>{lastIds.requestId}</Code>
<Button onClick={() => copy(lastIds.requestId)}>Copy</Button>
</div>
{lastIds.correlationId ? (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>correlation_id</span>
<Code>{lastIds.correlationId}</Code>
<Button onClick={() => copy(lastIds.correlationId ?? '')}>Copy</Button>
<Button
onClick={() => openGrafanaLogs(lastIds.correlationId ?? '')}
disabled={!grafana.base}
>
Investigate
</Button>
</div>
) : null}
</div>
) : null}
</header>
<Outlet />
</div>
</div>
)
}