Add client-side routing and build Infrastructure page
Wires up react-router-dom so the sidebar nav actually navigates between pages, with route-aware active highlighting and dynamic page titles. Extracts Glance content into its own page component and adds a new Infrastructure page matching the mockup: status cards, resource distribution/cost breakdown donuts, infra map, top resources by utilization, resource trend chart, recent activity, and footer stats.
This commit is contained in:
parent
b97a14a682
commit
ec04f568dd
8 changed files with 455 additions and 63 deletions
58
package-lock.json
generated
58
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router-dom": "^7.18.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwindcss": "^4.3.0"
|
||||
},
|
||||
|
|
@ -1697,6 +1698,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -2977,6 +2991,44 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
|
||||
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
|
||||
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
|
||||
|
|
@ -3077,6 +3129,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router-dom": "^7.18.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwindcss": "^4.3.0"
|
||||
},
|
||||
|
|
|
|||
51
src/App.tsx
51
src/App.tsx
|
|
@ -1,9 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import TopBar from './components/TopBar'
|
||||
import StatusCards from './components/StatusCards'
|
||||
import MiddleRow from './components/MiddleRow'
|
||||
import BottomRow from './components/BottomRow'
|
||||
import Glance from './pages/Glance'
|
||||
import Infrastructure from './pages/Infrastructure'
|
||||
|
||||
function App() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
|
|
@ -26,47 +26,10 @@ function App() {
|
|||
className="flex w-full flex-col overflow-hidden"
|
||||
style={{ height: 'calc(100vh - 56px)', scrollbarWidth: 'none', padding: '16px 24px 24px 24px', gap: '20px' }}
|
||||
>
|
||||
{/* Hero + KPI overlap — KPI bottom aligns with banner bottom */}
|
||||
<div className="relative w-full shrink-0 overflow-hidden" style={{ height: '240px' }}>
|
||||
<img
|
||||
src="/archnest-hero-banner.png"
|
||||
alt="ArchNest Banner"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center 35%',
|
||||
maskImage: 'linear-gradient(to bottom, black 0%, black 45%, transparent 95%)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 45%, transparent 95%)',
|
||||
}}
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget
|
||||
target.style.display = 'none'
|
||||
target.parentElement!.classList.add('bg-card')
|
||||
}}
|
||||
/>
|
||||
{/* Side vignette so the rectangular image blends into the page edges */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(ellipse 75% 100% at center, transparent 55%, var(--color-page) 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* KPI cards positioned so their bottom edge aligns with banner bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-4">
|
||||
<StatusCards />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Row — stretches to fill available vertical space */}
|
||||
<div className="min-h-0 flex-1">
|
||||
<MiddleRow />
|
||||
</div>
|
||||
|
||||
{/* Bottom Row — anchored to the bottom */}
|
||||
<div className="shrink-0">
|
||||
<BottomRow />
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/" element={<Glance />} />
|
||||
<Route path="/infrastructure" element={<Infrastructure />} />
|
||||
</Routes>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useLocation, Link } from 'react-router-dom'
|
||||
import {
|
||||
LayoutGrid,
|
||||
Server,
|
||||
|
|
@ -15,16 +16,17 @@ interface SidebarProps {
|
|||
}
|
||||
|
||||
const navItems = [
|
||||
{ icon: LayoutGrid, label: 'Glance', route: '/', active: true },
|
||||
{ icon: Server, label: 'Infrastructure', route: '/infrastructure', active: false },
|
||||
{ icon: Globe, label: 'Network', route: '/network', active: false },
|
||||
{ icon: Bookmark, label: 'BookNest', route: '/booknest', active: false },
|
||||
{ icon: Terminal, label: 'Terminal', route: '/terminal', active: false },
|
||||
{ icon: Settings, label: 'Settings', route: '/settings', active: false },
|
||||
{ icon: LayoutGrid, label: 'Glance', route: '/' },
|
||||
{ icon: Server, label: 'Infrastructure', route: '/infrastructure' },
|
||||
{ icon: Globe, label: 'Network', route: '/network' },
|
||||
{ icon: Bookmark, label: 'BookNest', route: '/booknest' },
|
||||
{ icon: Terminal, label: 'Terminal', route: '/terminal' },
|
||||
{ icon: Settings, label: 'Settings', route: '/settings' },
|
||||
]
|
||||
|
||||
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const width = collapsed ? 64 : 200
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<aside
|
||||
|
|
@ -69,37 +71,38 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
|||
<nav className="flex-1 flex flex-col gap-2 w-full" style={{ padding: collapsed ? '0 8px' : '0 12px' }}>
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = location.pathname === item.route
|
||||
return (
|
||||
<a
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.route}
|
||||
to={item.route}
|
||||
className={`relative flex items-center no-underline transition-all duration-200 ${collapsed ? 'justify-center' : ''}`}
|
||||
style={{
|
||||
color: item.active ? '#C8A434' : '#7A7D85',
|
||||
color: active ? '#C8A434' : '#7A7D85',
|
||||
gap: collapsed ? '0' : '12px',
|
||||
padding: collapsed ? '12px 0' : '12px 14px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: item.active ? 'rgba(200,164,52,0.1)' : 'transparent',
|
||||
border: item.active ? '1px solid rgba(200,164,52,0.18)' : '1px solid transparent',
|
||||
boxShadow: item.active ? '0 0 14px rgba(200,164,52,0.06)' : 'none',
|
||||
backgroundColor: active ? 'rgba(200,164,52,0.1)' : 'transparent',
|
||||
border: active ? '1px solid rgba(200,164,52,0.18)' : '1px solid transparent',
|
||||
boxShadow: active ? '0 0 14px rgba(200,164,52,0.06)' : 'none',
|
||||
}}
|
||||
title={collapsed ? item.label : undefined}
|
||||
onMouseEnter={(e) => { if (!item.active) { e.currentTarget.style.backgroundColor = 'rgba(200,164,52,0.05)'; e.currentTarget.style.color = '#C8A434' } }}
|
||||
onMouseLeave={(e) => { if (!item.active) { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = '#7A7D85' } }}
|
||||
onMouseEnter={(e) => { if (!active) { e.currentTarget.style.backgroundColor = 'rgba(200,164,52,0.05)'; e.currentTarget.style.color = '#C8A434' } }}
|
||||
onMouseLeave={(e) => { if (!active) { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = '#7A7D85' } }}
|
||||
>
|
||||
{item.active && (
|
||||
{active && (
|
||||
<div
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2"
|
||||
style={{ width: '3px', height: '22px', backgroundColor: '#C8A434', borderRadius: '0 3px 3px 0', boxShadow: '0 0 6px rgba(200,164,52,0.5)' }}
|
||||
/>
|
||||
)}
|
||||
<Icon className="h-5 w-5 shrink-0" strokeWidth={item.active ? 2 : 1.5} />
|
||||
<Icon className="h-5 w-5 shrink-0" strokeWidth={active ? 2 : 1.5} />
|
||||
{!collapsed && (
|
||||
<span className="truncate leading-tight font-medium" style={{ fontSize: '13px' }}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Search, Bell, ChevronDown, User, Palette, LogOut, Shield, HelpCircle } from 'lucide-react'
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
'/': 'Glance',
|
||||
'/infrastructure': 'Infrastructure',
|
||||
'/network': 'Network',
|
||||
'/booknest': 'BookNest',
|
||||
'/terminal': 'Terminal',
|
||||
'/settings': 'Settings',
|
||||
}
|
||||
|
||||
export default function TopBar() {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const location = useLocation()
|
||||
const title = pageTitles[location.pathname] ?? 'Glance'
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
|
|
@ -19,7 +31,7 @@ export default function TopBar() {
|
|||
<header className="h-14 flex items-center px-6 bg-page sticky top-0 z-40">
|
||||
{/* Page Title — pushed away from sidebar edge */}
|
||||
<h1 className="text-[18px] font-bold uppercase tracking-wide" style={{ color: '#C8A434', marginLeft: '20px' }}>
|
||||
Glance
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{/* Center section — Search bar */}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
|
|
|||
51
src/pages/Glance.tsx
Normal file
51
src/pages/Glance.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import StatusCards from '../components/StatusCards'
|
||||
import MiddleRow from '../components/MiddleRow'
|
||||
import BottomRow from '../components/BottomRow'
|
||||
|
||||
export default function Glance() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero + KPI overlap — KPI bottom aligns with banner bottom */}
|
||||
<div className="relative w-full shrink-0 overflow-hidden" style={{ height: '240px' }}>
|
||||
<img
|
||||
src="/archnest-hero-banner.png"
|
||||
alt="ArchNest Banner"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center 35%',
|
||||
maskImage: 'linear-gradient(to bottom, black 0%, black 45%, transparent 95%)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 45%, transparent 95%)',
|
||||
}}
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget
|
||||
target.style.display = 'none'
|
||||
target.parentElement!.classList.add('bg-card')
|
||||
}}
|
||||
/>
|
||||
{/* Side vignette so the rectangular image blends into the page edges */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(ellipse 75% 100% at center, transparent 55%, var(--color-page) 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* KPI cards positioned so their bottom edge aligns with banner bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-4">
|
||||
<StatusCards />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Row — stretches to fill available vertical space */}
|
||||
<div className="min-h-0 flex-1">
|
||||
<MiddleRow />
|
||||
</div>
|
||||
|
||||
{/* Bottom Row — anchored to the bottom */}
|
||||
<div className="shrink-0">
|
||||
<BottomRow />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
301
src/pages/Infrastructure.tsx
Normal file
301
src/pages/Infrastructure.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import { useState } from 'react'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, LineChart, Line, XAxis } from 'recharts'
|
||||
import { Plus, Server, Activity, AlertTriangle, DollarSign, Globe2 } from 'lucide-react'
|
||||
|
||||
const subTabs = ['Overview', 'Compute', 'Storage', 'Network', 'Database', 'Containers', 'Load Balancers', 'Backups', 'DNS', 'Tags']
|
||||
|
||||
const statusCards = [
|
||||
{ label: 'Total Resources', value: '128', icon: Server, sub: '+4 this week' },
|
||||
{ label: 'Healthy', value: '124', icon: Activity, sub: '96.9%', color: '#2ECC71' },
|
||||
{ label: 'Warnings', value: '3', icon: AlertTriangle, sub: 'Needs attention', color: '#E67E22' },
|
||||
{ label: 'Critical', value: '1', icon: AlertTriangle, sub: 'Action required', color: '#E74C3C' },
|
||||
{ label: 'Monthly Cost', value: '$24,560.75', icon: DollarSign, sub: '↑ 3.2% MTD' },
|
||||
]
|
||||
|
||||
const distributionData = [
|
||||
{ name: 'Compute', value: 42, color: '#C8A434' },
|
||||
{ name: 'Storage', value: 28, color: '#E67E22' },
|
||||
{ name: 'Database', value: 18, color: '#2ECC71' },
|
||||
{ name: 'Network', value: 12, color: '#7A7D85' },
|
||||
]
|
||||
|
||||
const costData = [
|
||||
{ name: 'Compute', value: 38, color: '#C8A434' },
|
||||
{ name: 'Storage', value: 22, color: '#E67E22' },
|
||||
{ name: 'Database', value: 25, color: '#2ECC71' },
|
||||
{ name: 'Network', value: 15, color: '#7A7D85' },
|
||||
]
|
||||
|
||||
const regions = [
|
||||
{ name: 'us-east-1', x: '22%', y: '38%', resources: 42 },
|
||||
{ name: 'us-west-2', x: '12%', y: '42%', resources: 18 },
|
||||
{ name: 'eu-west-1', x: '48%', y: '30%', resources: 31 },
|
||||
{ name: 'ap-southeast-1', x: '78%', y: '58%', resources: 22 },
|
||||
{ name: 'sa-east-1', x: '32%', y: '72%', resources: 9 },
|
||||
{ name: 'ap-northeast-1', x: '85%', y: '40%', resources: 6 },
|
||||
]
|
||||
|
||||
const topResources = [
|
||||
{ label: 'db-primary-01', percentage: 92 },
|
||||
{ label: 'app-server-03', percentage: 85 },
|
||||
{ label: 'cache-cluster-02', percentage: 78 },
|
||||
{ label: 'web-frontend-lb', percentage: 64 },
|
||||
{ label: 'batch-worker-04', percentage: 51 },
|
||||
]
|
||||
|
||||
const trendData = Array.from({ length: 14 }, (_, i) => ({
|
||||
day: i,
|
||||
compute: 60 + i * 0.6 + Math.sin(i / 2) * 3,
|
||||
storage: 35 + i * 0.4 + Math.cos(i / 3) * 2,
|
||||
database: 20 + i * 0.2 + Math.sin(i / 4) * 1.5,
|
||||
}))
|
||||
|
||||
const activities = [
|
||||
{ title: 'Auto-scaling triggered', source: 'App Server Group', time: '4m ago' },
|
||||
{ title: 'Resource provisioned', source: 'db-replica-02', time: '19m ago' },
|
||||
{ title: 'Health check failed', source: 'cache-cluster-02', time: '27m ago' },
|
||||
{ title: 'Tag updated', source: '12 resources', time: '1h ago' },
|
||||
]
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)',
|
||||
transition: 'border-color 0.2s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}
|
||||
|
||||
const sectionTitle: React.CSSProperties = {
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '1.5px',
|
||||
color: '#7A7D85',
|
||||
fontWeight: 500,
|
||||
marginBottom: '16px',
|
||||
}
|
||||
|
||||
function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center gap-4">
|
||||
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}>
|
||||
{data.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.color} stroke="none" />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
{centerLabel && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{data.map((entry) => (
|
||||
<div key={entry.name} className="flex items-center gap-2">
|
||||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '12px', color: '#E8E6E0', width: '70px' }}>{entry.name}</span>
|
||||
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Infrastructure() {
|
||||
const [activeTab, setActiveTab] = useState('Overview')
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Sub-tabs + Add Resource */}
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-1 overflow-x-auto" style={{ scrollbarWidth: 'none' }}>
|
||||
{subTabs.map((tab) => {
|
||||
const active = tab === activeTab
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className="cursor-pointer bg-transparent border-none transition-colors whitespace-nowrap"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
padding: '8px 14px',
|
||||
borderRadius: '8px',
|
||||
color: active ? '#C8A434' : '#7A7D85',
|
||||
backgroundColor: active ? 'rgba(200,164,52,0.1)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: '#0A0B0D',
|
||||
backgroundColor: '#C8A434',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '9px 16px',
|
||||
boxShadow: '0 0 14px rgba(200,164,52,0.2)',
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add Resource
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status Cards */}
|
||||
<div className="grid w-full grid-cols-5 gap-5 shrink-0">
|
||||
{statusCards.map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<div key={card.label} style={{ ...cardBase, padding: '16px' }} className="hover:!border-gold/20">
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
||||
{card.label}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} style={{ color: card.color ?? '#C8A434' }} />
|
||||
<span style={{ fontSize: '22px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{card.value}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '10px', color: card.color ?? '#7A7D85', marginTop: '6px' }}>{card.sub}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Middle Row */}
|
||||
<div className="min-h-0 flex-1">
|
||||
<div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
|
||||
{/* Resource Distribution */}
|
||||
<div style={cardBase} className="hover:!border-gold/15">
|
||||
<div className="relative z-10 flex flex-1 flex-col">
|
||||
<h3 style={sectionTitle}>Resource Distribution</h3>
|
||||
<Donut data={distributionData} centerLabel="128" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Infrastructure Map */}
|
||||
<div style={cardBase} className="hover:!border-gold/15">
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.03, backgroundImage: 'linear-gradient(rgba(200,164,52,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(200,164,52,0.3) 1px, transparent 1px)', backgroundSize: '40px 40px', pointerEvents: 'none' }} />
|
||||
<div className="relative z-10 flex flex-1 flex-col">
|
||||
<h3 style={sectionTitle}>Infrastructure Map</h3>
|
||||
<div className="relative flex-1" style={{ minHeight: '140px' }}>
|
||||
<Globe2 size={120} strokeWidth={0.6} style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', color: 'rgba(200,164,52,0.08)' }} />
|
||||
{regions.map((r) => (
|
||||
<div key={r.name} className="absolute" style={{ left: r.x, top: r.y, transform: 'translate(-50%, -50%)' }} title={`${r.name}: ${r.resources} resources`}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#C8A434', boxShadow: '0 0 10px rgba(200,164,52,0.7)' }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Resources by Utilization */}
|
||||
<div style={cardBase} className="hover:!border-gold/15">
|
||||
<div className="relative z-10 flex flex-1 flex-col">
|
||||
<h3 style={sectionTitle}>Top Resources by Utilization</h3>
|
||||
<div className="flex flex-1 flex-col justify-around gap-4">
|
||||
{topResources.map((res) => {
|
||||
const color = res.percentage >= 90 ? '#E74C3C' : res.percentage >= 70 ? '#E67E22' : '#C8A434'
|
||||
return (
|
||||
<div key={res.label}>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span style={{ fontSize: '12px', color: '#E8E6E0' }}>{res.label}</span>
|
||||
<span style={{ fontSize: '11px', color: '#7A7D85' }}>{res.percentage}%</span>
|
||||
</div>
|
||||
<div style={{ height: '6px', backgroundColor: 'rgba(30,32,37,0.8)', borderRadius: '3px', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${res.percentage}%`, backgroundColor: color, borderRadius: '3px', transition: 'width 0.8s ease' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Row */}
|
||||
<div className="shrink-0">
|
||||
<div className="grid w-full grid-cols-[1.4fr_1fr_1fr] gap-6">
|
||||
{/* Resource Trend */}
|
||||
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||
<div className="relative z-10">
|
||||
<h3 style={sectionTitle}>Resource Trend</h3>
|
||||
<div style={{ height: '100px' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||
<XAxis dataKey="day" hide />
|
||||
<Line type="monotone" dataKey="compute" stroke="#C8A434" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
|
||||
<Line type="monotone" dataKey="storage" stroke="#E67E22" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
|
||||
<Line type="monotone" dataKey="database" stroke="#2ECC71" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cost Breakdown */}
|
||||
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<h3 style={sectionTitle}>Cost Breakdown (MTD)</h3>
|
||||
<Donut data={costData} centerLabel="$24.5K" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||
<div className="relative z-10">
|
||||
<h3 style={sectionTitle}>Recent Activity</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{activities.map((item, i) => (
|
||||
<div key={i} className="flex items-start justify-between gap-3">
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: '12px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</p>
|
||||
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>
|
||||
</div>
|
||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer stats bar */}
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-center gap-3"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#7A7D85',
|
||||
padding: '8px 0',
|
||||
borderTop: '1px solid rgba(200,164,52,0.08)',
|
||||
}}
|
||||
>
|
||||
<span>AWS</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span>6 Regions</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span>18 AZs</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span>128 Resources</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span style={{ color: '#2ECC71' }}>98.7% Health</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span>$24,560.75 MTD</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
||||
<span style={{ color: '#E67E22' }}>2 Alerts</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue