feat(control-ui): modernize UX with premium Ultrabase design system
This commit is contained in:
@@ -1,28 +1,61 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom'
|
||||
import { getLastRequestIds } from '../api/client'
|
||||
import { Button, Code, TextInput } from '../components/primitives'
|
||||
import { Icons } from '../components/ui/Icons'
|
||||
|
||||
type NavItem = {
|
||||
label: string
|
||||
to: string
|
||||
icon: React.ComponentType<any>
|
||||
}
|
||||
|
||||
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: 'Documents', to: '/documents' },
|
||||
{ label: 'Scale & Placement', to: '/scale-placement' },
|
||||
{ label: 'Deployments', to: '/deployments' },
|
||||
{ label: 'Observability', to: '/observability' },
|
||||
{ label: 'Platform Drift', to: '/drift' },
|
||||
{ label: 'Audit Log', to: '/audit-log' },
|
||||
{ label: 'Settings', to: '/settings' },
|
||||
type NavCategory = {
|
||||
label: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
const navCategories: NavCategory[] = [
|
||||
{
|
||||
label: 'Platform',
|
||||
items: [
|
||||
{ label: 'Overview', to: '/', icon: Icons.Activity },
|
||||
{ label: 'Tenants', to: '/tenants', icon: Icons.Tenants },
|
||||
{ label: 'Fleet Nodes', to: '/fleet', icon: Icons.Fleet },
|
||||
{ label: 'Observability', to: '/observability', icon: Icons.Observability },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Infrastructure',
|
||||
items: [
|
||||
{ label: 'Scale & Placement', to: '/scale-placement', icon: Icons.Topology },
|
||||
{ label: 'Deployments', to: '/deployments', icon: Icons.Deployments },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Resources',
|
||||
items: [
|
||||
{ label: 'Config Registry', to: '/config', icon: Icons.Config },
|
||||
{ label: 'Type Definitions', to: '/definitions', icon: Icons.Definitions },
|
||||
{ label: 'Documents', to: '/documents', icon: Icons.Storage },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Governance',
|
||||
items: [
|
||||
{ label: 'Audit Log', to: '/audit-log', icon: Icons.Audit },
|
||||
{ label: 'Platform Drift', to: '/drift', icon: Icons.Drift },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Management',
|
||||
items: [
|
||||
{ label: 'Users', to: '/users', icon: Icons.Users },
|
||||
{ label: 'Sessions', to: '/sessions', icon: Icons.Sessions },
|
||||
{ label: 'Roles & Permissions', to: '/roles-permissions', icon: Icons.ShieldAlert },
|
||||
{ label: 'Settings', to: '/settings', icon: Icons.Settings },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function normalizePath(pathname: string) {
|
||||
@@ -33,7 +66,7 @@ function normalizePath(pathname: string) {
|
||||
|
||||
export function Layout() {
|
||||
const location = useLocation()
|
||||
const active = normalizePath(location.pathname)
|
||||
const activePath = normalizePath(location.pathname)
|
||||
const [query, setQuery] = useState('')
|
||||
const lastIds = getLastRequestIds()
|
||||
|
||||
@@ -74,111 +107,251 @@ export function Layout() {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}>
|
||||
<aside
|
||||
const NavLink = ({ item }: { item: NavItem }) => {
|
||||
const active = activePath === normalizePath(item.to)
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<Link
|
||||
to={item.to}
|
||||
style={{
|
||||
width: 260,
|
||||
borderRight: '1px solid #eee',
|
||||
padding: 16,
|
||||
background: '#fafafa',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
|
||||
background: active ? 'var(--bg-tertiary)' : 'transparent',
|
||||
fontWeight: active ? 500 : 400,
|
||||
textDecoration: 'none',
|
||||
fontSize: '14px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.05)'
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
if (!active) e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<span style={{ color: active ? 'var(--accent-primary)' : 'inherit', display: 'flex' }}>
|
||||
<Icon />
|
||||
</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<header
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg-primary)' }}>
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
style={{
|
||||
width: 240,
|
||||
borderRight: '1px solid var(--border-primary)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'var(--bg-primary)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
height: '100vh',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #eee',
|
||||
padding: 16,
|
||||
padding: '24px',
|
||||
borderBottom: '1px solid var(--border-primary)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||
<div style={{ width: 420, maxWidth: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
background: 'var(--accent-primary)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#000',
|
||||
}}
|
||||
>
|
||||
<Icons.Zap />
|
||||
</div>
|
||||
<span style={{ fontWeight: 700, fontSize: '18px', letterSpacing: '-0.02em' }}>
|
||||
Cloudlysis
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '20px 12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{navCategories.map((cat) => (
|
||||
<div key={cat.label} style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '0 12px',
|
||||
marginBottom: '4px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
{cat.label}
|
||||
</div>
|
||||
{cat.items.map((item) => (
|
||||
<NavLink key={item.to} item={item} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
borderTop: '1px solid var(--border-primary)',
|
||||
background: 'var(--bg-secondary)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--bg-tertiary)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
OP
|
||||
</div>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
Operator
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Administrator</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
<header
|
||||
style={{
|
||||
borderBottom: '1px solid var(--border-primary)',
|
||||
padding: '16px 32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
background: 'var(--bg-primary)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ width: 480, maxWidth: '100%' }}>
|
||||
<TextInput
|
||||
ariaLabel="Global search"
|
||||
placeholder="Search request/correlation/trace id"
|
||||
placeholder="Search correlation/trace id..."
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'Enter') {
|
||||
const id = query.trim()
|
||||
if (!id) return
|
||||
openGrafanaLogs(id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const id = query.trim()
|
||||
if (!id) return
|
||||
openGrafanaLogs(id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
disabled={!grafana.base}
|
||||
icon={<Icons.Activity style={{ width: 14, height: 14 }} />}
|
||||
>
|
||||
Logs
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const id = query.trim()
|
||||
if (!id) return
|
||||
openGrafanaTrace(id)
|
||||
}}
|
||||
disabled={!grafana.base}
|
||||
icon={<Icons.Topology style={{ width: 14, height: 14 }} />}
|
||||
>
|
||||
Trace
|
||||
</Button>
|
||||
</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>
|
||||
{lastIds && (
|
||||
<div style={{ display: 'flex', gap: '24px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>request_id</span>
|
||||
<Code>{lastIds.requestId}</Code>
|
||||
<Button onClick={() => copy(lastIds.requestId)}>Copy</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => copy(lastIds.requestId)} style={{ padding: '2px 4px' }}>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
{lastIds.correlationId ? (
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 12, color: '#666' }}>correlation_id</span>
|
||||
{lastIds.correlationId && (
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>correlation_id</span>
|
||||
<Code>{lastIds.correlationId}</Code>
|
||||
<Button onClick={() => copy(lastIds.correlationId ?? '')}>Copy</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => copy(lastIds.correlationId || '')} style={{ padding: '2px 4px' }}>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => openGrafanaLogs(lastIds.correlationId ?? '')}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => openGrafanaLogs(lastIds.correlationId || '')}
|
||||
disabled={!grafana.base}
|
||||
style={{ fontSize: '11px', padding: '2px 8px' }}
|
||||
>
|
||||
Investigate
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</header>
|
||||
<Outlet />
|
||||
|
||||
<main style={{ padding: '32px', flex: 1 }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user