Replace mock data on Glance and Infrastructure with real backend data
Adds an events table + logEvent helper for a genuine activity log, and a /api/integrations/resources aggregate endpoint backed by a new optional listResources adapter method (implemented for Docker via its containers API). StatusCards, MiddleRow, BottomRow, and Infrastructure now render real integration/resource/event data instead of hardcoded numbers, with empty states where no data source exists yet (AWS cost, historical trends). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
This commit is contained in:
parent
b49f8ac8f5
commit
3b920fcfb2
13 changed files with 430 additions and 336 deletions
|
|
@ -57,4 +57,16 @@ db.exec(`
|
||||||
last_checked_at TEXT,
|
last_checked_at TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
source TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
export function logEvent(type: string, title: string, source?: string | null) {
|
||||||
|
db.prepare('INSERT INTO events (type, title, source) VALUES (?, ?, ?)').run(type, title, source ?? null)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { IntegrationAdapter } from './types.js'
|
import type { IntegrationAdapter, Resource } from './types.js'
|
||||||
|
|
||||||
export const docker: IntegrationAdapter = {
|
export const docker: IntegrationAdapter = {
|
||||||
async testConnection(config) {
|
async testConnection(config) {
|
||||||
|
|
@ -12,4 +12,17 @@ export const docker: IntegrationAdapter = {
|
||||||
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' }
|
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listResources(config): Promise<Resource[]> {
|
||||||
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
||||||
|
if (!baseUrl) return []
|
||||||
|
const res = await fetch(`${baseUrl}/containers/json?all=true`)
|
||||||
|
if (!res.ok) return []
|
||||||
|
const containers = (await res.json()) as { Names: string[]; State: string }[]
|
||||||
|
return containers.map((c) => ({
|
||||||
|
name: c.Names[0]?.replace(/^\//, '') ?? 'unknown',
|
||||||
|
status: c.State === 'running' ? 'healthy' : c.State === 'restarting' ? 'warning' : 'critical',
|
||||||
|
detail: c.State,
|
||||||
|
}))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ export interface TestResult {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
name: string
|
||||||
|
status: 'healthy' | 'warning' | 'critical' | 'unknown'
|
||||||
|
detail?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IntegrationAdapter {
|
export interface IntegrationAdapter {
|
||||||
testConnection(config: IntegrationConfig, secrets: Record<string, string>): Promise<TestResult>
|
testConnection(config: IntegrationConfig, secrets: Record<string, string>): Promise<TestResult>
|
||||||
|
listResources?(config: IntegrationConfig, secrets: Record<string, string>): Promise<Resource[]>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { db } from '../db/index.js'
|
import { db, logEvent } from '../db/index.js'
|
||||||
|
|
||||||
const credentialsSchema = z.object({
|
const credentialsSchema = z.object({
|
||||||
username: z.string().min(3).max(64),
|
username: z.string().min(3).max(64),
|
||||||
|
|
@ -29,6 +29,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||||
.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
|
.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
|
||||||
.run(username, passwordHash)
|
.run(username, passwordHash)
|
||||||
const token = app.jwt.sign({ sub: Number(result.lastInsertRowid), username })
|
const token = app.jwt.sign({ sub: Number(result.lastInsertRowid), username })
|
||||||
|
logEvent('account_created', `Account created for ${username}`)
|
||||||
return { token }
|
return { token }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -45,6 +46,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||||
return reply.code(401).send({ error: 'Invalid username or password' })
|
return reply.code(401).send({ error: 'Invalid username or password' })
|
||||||
}
|
}
|
||||||
const token = app.jwt.sign({ sub: user.id, username: user.username })
|
const token = app.jwt.sign({ sub: user.id, username: user.username })
|
||||||
|
logEvent('user_login', `${user.username} logged in`)
|
||||||
return { token }
|
return { token }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { db } from '../db/index.js'
|
import { db, logEvent } from '../db/index.js'
|
||||||
|
|
||||||
const bookmarkSchema = z.object({
|
const bookmarkSchema = z.object({
|
||||||
categoryId: z.number().int().nullable().optional(),
|
categoryId: z.number().int().nullable().optional(),
|
||||||
|
|
@ -48,6 +48,7 @@ export async function bookmarkRoutes(app: FastifyInstance) {
|
||||||
'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)'
|
||||||
)
|
)
|
||||||
.run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0)
|
.run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0)
|
||||||
|
logEvent('bookmark_created', `Bookmark added: ${title}`)
|
||||||
return reply.code(201).send({ id: result.lastInsertRowid })
|
return reply.code(201).send({ id: result.lastInsertRowid })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -72,7 +73,9 @@ export async function bookmarkRoutes(app: FastifyInstance) {
|
||||||
|
|
||||||
app.delete('/api/bookmarks/:id', async (req, reply) => {
|
app.delete('/api/bookmarks/:id', async (req, reply) => {
|
||||||
const id = Number((req.params as { id: string }).id)
|
const id = Number((req.params as { id: string }).id)
|
||||||
|
const existing = db.prepare('SELECT title FROM bookmarks WHERE id = ?').get(id) as { title: string } | undefined
|
||||||
db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id)
|
db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id)
|
||||||
|
if (existing) logEvent('bookmark_deleted', `Bookmark removed: ${existing.title}`)
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
backend/src/routes/events.ts
Normal file
13
backend/src/routes/events.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { FastifyInstance } from 'fastify'
|
||||||
|
import { db } from '../db/index.js'
|
||||||
|
|
||||||
|
export async function eventRoutes(app: FastifyInstance) {
|
||||||
|
app.addHook('onRequest', app.authenticate)
|
||||||
|
|
||||||
|
app.get('/api/events', async (req) => {
|
||||||
|
const query = req.query as { limit?: string }
|
||||||
|
const limit = Math.min(Number(query.limit) || 20, 100)
|
||||||
|
const events = db.prepare('SELECT * FROM events ORDER BY created_at DESC, id DESC LIMIT ?').all(limit)
|
||||||
|
return { events }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { db } from '../db/index.js'
|
import { db, logEvent } from '../db/index.js'
|
||||||
import { encryptSecret, decryptSecret } from '../db/crypto.js'
|
import { encryptSecret, decryptSecret } from '../db/crypto.js'
|
||||||
import { adapterRegistry } from '../integrations/registry.js'
|
import { adapterRegistry } from '../integrations/registry.js'
|
||||||
import type { IntegrationType } from '../integrations/types.js'
|
import type { IntegrationType, Resource } from '../integrations/types.js'
|
||||||
|
|
||||||
const integrationTypes = [
|
const integrationTypes = [
|
||||||
'proxmox',
|
'proxmox',
|
||||||
|
|
@ -80,6 +80,7 @@ export async function integrationRoutes(app: FastifyInstance) {
|
||||||
insertSecret.run(integrationId, key, encryptSecret(value))
|
insertSecret.run(integrationId, key, encryptSecret(value))
|
||||||
}
|
}
|
||||||
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(integrationId) as IntegrationRow
|
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(integrationId) as IntegrationRow
|
||||||
|
logEvent('integration_created', `${name} integration added`, type)
|
||||||
return reply.code(201).send({ integration: serialize(row) })
|
return reply.code(201).send({ integration: serialize(row) })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -116,7 +117,9 @@ export async function integrationRoutes(app: FastifyInstance) {
|
||||||
|
|
||||||
app.delete('/api/integrations/:id', async (req, reply) => {
|
app.delete('/api/integrations/:id', async (req, reply) => {
|
||||||
const id = Number((req.params as { id: string }).id)
|
const id = Number((req.params as { id: string }).id)
|
||||||
|
const existing = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as IntegrationRow | undefined
|
||||||
db.prepare('DELETE FROM integrations WHERE id = ?').run(id)
|
db.prepare('DELETE FROM integrations WHERE id = ?').run(id)
|
||||||
|
if (existing) logEvent('integration_deleted', `${existing.name} integration removed`, existing.type)
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -136,6 +139,25 @@ export async function integrationRoutes(app: FastifyInstance) {
|
||||||
status,
|
status,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
logEvent('integration_tested', `${row.name} test ${result.ok ? 'succeeded' : 'failed'}`, row.type)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/api/integrations/resources', async () => {
|
||||||
|
const rows = db.prepare("SELECT * FROM integrations WHERE enabled = 1 AND status = 'connected'").all() as IntegrationRow[]
|
||||||
|
const resources: (Resource & { integration: string })[] = []
|
||||||
|
for (const row of rows) {
|
||||||
|
const adapter = adapterRegistry[row.type as IntegrationType]
|
||||||
|
if (!adapter.listResources) continue
|
||||||
|
const config = JSON.parse(row.config_json)
|
||||||
|
const secrets = loadSecrets(row.id)
|
||||||
|
try {
|
||||||
|
const found = await adapter.listResources(config, secrets)
|
||||||
|
for (const r of found) resources.push({ ...r, integration: row.name })
|
||||||
|
} catch {
|
||||||
|
// adapter unreachable — skip, connection test already surfaces this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { resources }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import jwt from '@fastify/jwt'
|
||||||
import { authRoutes } from './routes/auth.js'
|
import { authRoutes } from './routes/auth.js'
|
||||||
import { integrationRoutes } from './routes/integrations.js'
|
import { integrationRoutes } from './routes/integrations.js'
|
||||||
import { bookmarkRoutes } from './routes/bookmarks.js'
|
import { bookmarkRoutes } from './routes/bookmarks.js'
|
||||||
|
import { eventRoutes } from './routes/events.js'
|
||||||
|
|
||||||
const JWT_SECRET = process.env.ARCHNEST_JWT_SECRET
|
const JWT_SECRET = process.env.ARCHNEST_JWT_SECRET
|
||||||
if (!JWT_SECRET) {
|
if (!JWT_SECRET) {
|
||||||
|
|
@ -27,6 +28,7 @@ app.decorate('authenticate', async function (req, reply) {
|
||||||
await app.register(authRoutes)
|
await app.register(authRoutes)
|
||||||
await app.register(integrationRoutes)
|
await app.register(integrationRoutes)
|
||||||
await app.register(bookmarkRoutes)
|
await app.register(bookmarkRoutes)
|
||||||
|
await app.register(eventRoutes)
|
||||||
|
|
||||||
app.get('/api/health', async () => ({ ok: true }))
|
app.get('/api/health', async () => ({ ok: true }))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts'
|
import { useEffect, useState } from 'react'
|
||||||
import { ServerCog, DatabaseBackup, Rocket, FileText } from 'lucide-react'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Plug, ServerCog, BookMarked, Settings as SettingsIcon } from 'lucide-react'
|
||||||
const trafficData = Array.from({ length: 48 }, (_, i) => ({
|
import { api, type Integration } from '../lib/api'
|
||||||
time: i,
|
|
||||||
incoming: 800 + Math.sin(i / 6) * 300 + Math.random() * 150,
|
|
||||||
outgoing: 700 + Math.cos(i / 8) * 250 + Math.random() * 100,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{ icon: ServerCog, label: 'Add Server' },
|
{ icon: ServerCog, label: 'Add Integration', to: '/settings' },
|
||||||
{ icon: DatabaseBackup, label: 'Create Backup' },
|
{ icon: BookMarked, label: 'Add Bookmark', to: '/booknest' },
|
||||||
{ icon: Rocket, label: 'Deploy App' },
|
{ icon: Plug, label: 'Infrastructure', to: '/infrastructure' },
|
||||||
{ icon: FileText, label: 'View Logs' },
|
{ icon: SettingsIcon, label: 'Settings', to: '/settings' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const cardBase: React.CSSProperties = {
|
const cardBase: React.CSSProperties = {
|
||||||
|
|
@ -25,56 +21,48 @@ const cardBase: React.CSSProperties = {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusColor: Record<string, string> = {
|
||||||
|
connected: '#2ECC71',
|
||||||
|
error: '#E74C3C',
|
||||||
|
unknown: '#7A7D85',
|
||||||
|
}
|
||||||
|
|
||||||
export default function BottomRow() {
|
export default function BottomRow() {
|
||||||
|
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full grid-cols-[1.8fr_1fr] gap-6">
|
<div className="grid w-full grid-cols-[1.8fr_1fr] gap-6">
|
||||||
{/* Network Traffic */}
|
{/* Connected Integrations */}
|
||||||
<div style={cardBase} className="hover:!border-gold/15">
|
<div style={cardBase} className="hover:!border-gold/15">
|
||||||
{/* Background image at very low opacity */}
|
|
||||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.12, backgroundImage: 'url(/archnest-network-traffic-bg.png)', backgroundSize: 'cover', backgroundPosition: 'center', pointerEvents: 'none' }} />
|
|
||||||
{/* Gold top edge */}
|
|
||||||
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||||
Network Traffic
|
Connected Integrations
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-end gap-6">
|
{integrations === null ? (
|
||||||
<div style={{ flex: 1, height: '100px' }}>
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading…</p>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
) : integrations.length === 0 ? (
|
||||||
<AreaChart data={trafficData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No integrations added yet — add one in Settings.</p>
|
||||||
<defs>
|
) : (
|
||||||
<linearGradient id="trafficGold" x1="0" y1="0" x2="0" y2="1">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<stop offset="0%" stopColor="#C8A434" stopOpacity={0.25} />
|
{integrations.map((i) => (
|
||||||
<stop offset="100%" stopColor="#C8A434" stopOpacity={0.02} />
|
<div key={i.id} className="flex items-center gap-2.5" style={{ padding: '8px 10px', borderRadius: '8px', backgroundColor: 'rgba(255,255,255,0.02)' }}>
|
||||||
</linearGradient>
|
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: statusColor[i.status] ?? '#7A7D85', flexShrink: 0 }} />
|
||||||
<linearGradient id="trafficAmber" x1="0" y1="0" x2="0" y2="1">
|
<span style={{ fontSize: '13px', color: '#E8E6E0', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{i.name}</span>
|
||||||
<stop offset="0%" stopColor="#E67E22" stopOpacity={0.15} />
|
|
||||||
<stop offset="100%" stopColor="#E67E22" stopOpacity={0.02} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<Area type="monotone" dataKey="incoming" stroke="#C8A434" strokeWidth={1.5} fill="url(#trafficGold)" dot={false} isAnimationActive={true} animationDuration={1200} />
|
|
||||||
<Area type="monotone" dataKey="outgoing" stroke="rgba(230,126,34,0.6)" strokeWidth={1} fill="url(#trafficAmber)" dot={false} isAnimationActive={true} animationDuration={1200} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3 flex-shrink-0">
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Incoming</p>
|
|
||||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.23 Gbps</p>
|
|
||||||
<p style={{ fontSize: '11px', color: '#E74C3C' }}>↓ 12.4%</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Outgoing</p>
|
|
||||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.08 Gbps</p>
|
|
||||||
<p style={{ fontSize: '11px', color: '#2ECC71' }}>↑ 8.7%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Shortcuts — miniature control panels */}
|
{/* Shortcuts */}
|
||||||
<div style={cardBase} className="hover:!border-gold/15">
|
<div style={cardBase} className="hover:!border-gold/15">
|
||||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '24px 24px', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '24px 24px', pointerEvents: 'none' }} />
|
||||||
|
|
||||||
|
|
@ -88,6 +76,7 @@ export default function BottomRow() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
onClick={() => navigate(item.to)}
|
||||||
className="flex flex-col items-center gap-2 cursor-pointer bg-transparent transition-all duration-200 group/btn"
|
className="flex flex-col items-center gap-2 cursor-pointer bg-transparent transition-all duration-200 group/btn"
|
||||||
style={{ padding: '16px 8px', borderRadius: '10px', border: '1px solid rgba(200,164,52,0.08)', boxShadow: '0 0 12px rgba(200,164,52,0.02)' }}
|
style={{ padding: '16px 8px', borderRadius: '10px', border: '1px solid rgba(200,164,52,0.08)', boxShadow: '0 0 12px rgba(200,164,52,0.02)' }}
|
||||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'rgba(200,164,52,0.2)'; e.currentTarget.style.boxShadow = '0 0 16px rgba(200,164,52,0.06)' }}
|
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'rgba(200,164,52,0.2)'; e.currentTarget.style.boxShadow = '0 0 16px rgba(200,164,52,0.06)' }}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,6 @@
|
||||||
import { X, CircleCheck, Shield, Play, Settings, User } from 'lucide-react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { CircleCheck, AlertTriangle, Plug, Bookmark as BookmarkIcon, LogIn } from 'lucide-react'
|
||||||
const resources = [
|
import { api, type Event, type Resource, type Integration } from '../lib/api'
|
||||||
{ label: 'Compute', current: 18, max: 24, unit: '' },
|
|
||||||
{ label: 'Storage', current: 12.4, max: 20, unit: ' TB' },
|
|
||||||
{ label: 'Database', current: 8, max: 12, unit: '' },
|
|
||||||
{ label: 'Network', current: 98.7, max: 100, unit: '%' },
|
|
||||||
{ label: 'Containers', current: 32, max: 40, unit: '' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const activities = [
|
|
||||||
{ icon: CircleCheck, title: 'Backup completed', source: 'Database Cluster 01', time: '2m ago' },
|
|
||||||
{ icon: Shield, title: 'Security scan completed', source: 'Web Frontend', time: '8m ago' },
|
|
||||||
{ icon: Play, title: 'Instance launched', source: 'App Server 03', time: '15m ago' },
|
|
||||||
{ icon: Settings, title: 'Configuration updated', source: 'Load Balancer', time: '22m ago' },
|
|
||||||
{ icon: User, title: 'User login detected', source: 'admin@archnest.io', time: '35m ago' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const alerts = [
|
|
||||||
{ severity: 'high', title: 'High CPU Usage', source: 'App Server 02', time: '2m ago' },
|
|
||||||
{ severity: 'medium', title: 'Disk Space Low', source: 'Database Cluster 01', time: '15m ago' },
|
|
||||||
{ severity: 'medium', title: 'Unauthorized Login Attempt', source: 'Web Frontend', time: '32m ago' },
|
|
||||||
{ severity: 'medium', title: 'SSL Certificate Expiring', source: 'api.archnest.io', time: '1h ago' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const cardBase: React.CSSProperties = {
|
const cardBase: React.CSSProperties = {
|
||||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||||
|
|
@ -37,109 +16,143 @@ const cardBase: React.CSSProperties = {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBarColor(percentage: number) {
|
const statusColor: Record<string, string> = {
|
||||||
if (percentage >= 90) return '#E74C3C'
|
healthy: '#C8A434',
|
||||||
if (percentage >= 70) return '#E67E22'
|
warning: '#E67E22',
|
||||||
return '#C8A434'
|
critical: '#E74C3C',
|
||||||
|
unknown: '#7A7D85',
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventIcons: Record<string, typeof CircleCheck> = {
|
||||||
|
integration_created: Plug,
|
||||||
|
integration_tested: CircleCheck,
|
||||||
|
integration_deleted: Plug,
|
||||||
|
bookmark_created: BookmarkIcon,
|
||||||
|
bookmark_deleted: BookmarkIcon,
|
||||||
|
user_login: LogIn,
|
||||||
|
account_created: LogIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(iso: string) {
|
||||||
|
const diffMs = Date.now() - new Date(iso.replace(' ', 'T') + 'Z').getTime()
|
||||||
|
const mins = Math.floor(diffMs / 60000)
|
||||||
|
if (mins < 1) return 'just now'
|
||||||
|
if (mins < 60) return `${mins}m ago`
|
||||||
|
const hours = Math.floor(mins / 60)
|
||||||
|
if (hours < 24) return `${hours}h ago`
|
||||||
|
return `${Math.floor(hours / 24)}d ago`
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MiddleRow() {
|
export default function MiddleRow() {
|
||||||
|
const [resources, setResources] = useState<Resource[] | null>(null)
|
||||||
|
const [events, setEvents] = useState<Event[] | null>(null)
|
||||||
|
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.listResources().then(({ resources }) => setResources(resources))
|
||||||
|
api.listEvents(5).then(({ events }) => setEvents(events))
|
||||||
|
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const erroredIntegrations = integrations?.filter((i) => i.status === 'error') ?? []
|
||||||
|
const problemResources = resources?.filter((r) => r.status === 'warning' || r.status === 'critical') ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
|
<div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
|
||||||
{/* Resource Overview */}
|
{/* Resource Overview */}
|
||||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||||
{/* Subtle hex pattern overlay */}
|
|
||||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.03, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '20px 20px', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', inset: 0, opacity: 0.03, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '20px 20px', pointerEvents: 'none' }} />
|
||||||
{/* Gold top edge lighting */}
|
|
||||||
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
|
||||||
Resource Overview
|
Resource Overview
|
||||||
</h3>
|
</h3>
|
||||||
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
|
{resources === null ? (
|
||||||
<X size={14} />
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading…</p>
|
||||||
</button>
|
) : resources.length === 0 ? (
|
||||||
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Connect an integration in Settings to see live resources here.</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-1 flex-col justify-around gap-3" style={{ overflowY: 'auto' }}>
|
||||||
|
{resources.slice(0, 6).map((r, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2.5">
|
||||||
|
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: statusColor[r.status], flexShrink: 0 }} />
|
||||||
|
<span style={{ fontSize: '13px', color: '#E8E6E0', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name}</span>
|
||||||
|
<span style={{ fontSize: '10px', color: '#7A7D85', flexShrink: 0 }}>{r.integration}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col justify-around gap-4">
|
))}
|
||||||
{resources.map((res) => {
|
|
||||||
const percentage = res.unit === '%' ? res.current : (res.current / res.max) * 100
|
|
||||||
const displayValue = res.unit === '%' ? `${res.current}%` : `${res.current} / ${res.max}${res.unit}`
|
|
||||||
return (
|
|
||||||
<div key={res.label} className="flex items-center gap-3">
|
|
||||||
<span style={{ fontSize: '13px', color: '#E8E6E0', width: '90px' }}>{res.label}</span>
|
|
||||||
<div style={{ flex: 1, height: '6px', backgroundColor: 'rgba(30,32,37,0.8)', borderRadius: '3px', overflow: 'hidden' }}>
|
|
||||||
<div style={{ height: '100%', width: `${percentage}%`, backgroundColor: getBarColor(percentage), borderRadius: '3px', transition: 'width 0.8s ease' }} />
|
|
||||||
</div>
|
|
||||||
<span style={{ fontSize: '12px', color: '#7A7D85', width: '80px', textAlign: 'right' }}>{displayValue}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Activity — visually dominant */}
|
{/* Recent Activity */}
|
||||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||||
{/* City grid texture */}
|
|
||||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, 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 style={{ position: 'absolute', inset: 0, opacity: 0.02, 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' }} />
|
||||||
{/* Gold top edge */}
|
|
||||||
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.2), transparent)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.2), transparent)', pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
|
||||||
Recent Activity
|
Recent Activity
|
||||||
</h3>
|
</h3>
|
||||||
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
|
{events === null ? (
|
||||||
<X size={14} />
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading…</p>
|
||||||
</button>
|
) : events.length === 0 ? (
|
||||||
</div>
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
|
||||||
|
) : (
|
||||||
<div className="flex flex-1 flex-col justify-around gap-3">
|
<div className="flex flex-1 flex-col justify-around gap-3">
|
||||||
{activities.map((item, i) => {
|
{events.map((item) => {
|
||||||
const Icon = item.icon
|
const Icon = eventIcons[item.type] ?? CircleCheck
|
||||||
return (
|
return (
|
||||||
<div key={i} className="flex items-start gap-3">
|
<div key={item.id} className="flex items-start gap-3">
|
||||||
<div style={{ width: '28px', height: '28px', borderRadius: '6px', backgroundColor: 'rgba(200,164,52,0.06)', border: '1px solid rgba(200,164,52,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxShadow: '0 0 8px rgba(200,164,52,0.04)' }}>
|
<div style={{ width: '28px', height: '28px', borderRadius: '6px', backgroundColor: 'rgba(200,164,52,0.06)', border: '1px solid rgba(200,164,52,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxShadow: '0 0 8px rgba(200,164,52,0.04)' }}>
|
||||||
<Icon size={13} style={{ color: '#C8A434' }} />
|
<Icon size={13} style={{ color: '#C8A434' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</p>
|
<p style={{ fontSize: '13px', 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>
|
{item.source && <p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
|
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{timeAgo(item.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top Alerts */}
|
{/* Top Alerts */}
|
||||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||||
{/* Amber edge lighting */}
|
|
||||||
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(231,126,34,0.15), transparent)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(231,126,34,0.15), transparent)', pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
|
||||||
Top Alerts
|
Top Alerts
|
||||||
</h3>
|
</h3>
|
||||||
<a href="#" style={{ fontSize: '11px', color: '#C8A434', textDecoration: 'none' }}>View all</a>
|
{erroredIntegrations.length === 0 && problemResources.length === 0 ? (
|
||||||
</div>
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No alerts — everything connected is healthy.</p>
|
||||||
|
) : (
|
||||||
<div className="flex flex-1 flex-col justify-around gap-3">
|
<div className="flex flex-1 flex-col justify-around gap-3">
|
||||||
{alerts.map((alert, i) => (
|
{erroredIntegrations.map((i) => (
|
||||||
<div key={i} className="flex items-start gap-3">
|
<div key={`int-${i.id}`} className="flex items-start gap-3">
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0, marginTop: '5px', backgroundColor: alert.severity === 'high' ? '#E74C3C' : '#E67E22', boxShadow: alert.severity === 'high' ? '0 0 6px rgba(231,76,60,0.3)' : '0 0 6px rgba(230,126,34,0.2)' }} />
|
<AlertTriangle size={14} style={{ color: '#E74C3C', flexShrink: 0, marginTop: '2px' }} />
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.title}</p>
|
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500 }}>Connection failing</p>
|
||||||
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.source}</p>
|
<p style={{ fontSize: '11px', color: '#7A7D85' }}>{i.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{problemResources.map((r, idx) => (
|
||||||
|
<div key={`res-${idx}`} className="flex items-start gap-3">
|
||||||
|
<AlertTriangle size={14} style={{ color: r.status === 'critical' ? '#E74C3C' : '#E67E22', flexShrink: 0, marginTop: '2px' }} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500 }}>{r.name}</p>
|
||||||
|
<p style={{ fontSize: '11px', color: '#7A7D85' }}>{r.integration} · {r.detail ?? r.status}</p>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{alert.time}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Server, Shield, Network } from 'lucide-react'
|
import { useEffect, useState } from 'react'
|
||||||
import SparklineChart from './SparklineChart'
|
import { Server, Plug, BookMarked } from 'lucide-react'
|
||||||
import ProgressRing from './ProgressRing'
|
import ProgressRing from './ProgressRing'
|
||||||
|
import { api, type Integration, type Resource, type Bookmark } from '../lib/api'
|
||||||
|
|
||||||
const cardStyle: React.CSSProperties = {
|
const cardStyle: React.CSSProperties = {
|
||||||
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
||||||
|
|
@ -12,92 +13,96 @@ const cardStyle: React.CSSProperties = {
|
||||||
transition: 'border-color 0.2s ease',
|
transition: 'border-color 0.2s ease',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
|
fontSize: '10px',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '1.5px',
|
||||||
|
color: '#7A7D85',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: '8px',
|
||||||
|
}
|
||||||
|
|
||||||
export default function StatusCards() {
|
export default function StatusCards() {
|
||||||
|
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
|
||||||
|
const [resources, setResources] = useState<Resource[] | null>(null)
|
||||||
|
const [bookmarks, setBookmarks] = useState<Bookmark[] | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
|
||||||
|
api.listResources().then(({ resources }) => setResources(resources))
|
||||||
|
api.listBookmarks().then(({ bookmarks }) => setBookmarks(bookmarks))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const connected = integrations?.filter((i) => i.status === 'connected').length ?? 0
|
||||||
|
const errored = integrations?.filter((i) => i.status === 'error').length ?? 0
|
||||||
|
const total = integrations?.length ?? 0
|
||||||
|
|
||||||
|
const healthy = resources?.filter((r) => r.status === 'healthy').length ?? 0
|
||||||
|
const warning = resources?.filter((r) => r.status === 'warning').length ?? 0
|
||||||
|
const critical = resources?.filter((r) => r.status === 'critical').length ?? 0
|
||||||
|
const resourceTotal = resources?.length ?? 0
|
||||||
|
|
||||||
|
const favorites = bookmarks?.filter((b) => b.favorite).length ?? 0
|
||||||
|
|
||||||
|
const systemLabel = errored > 0 ? 'Issues Detected' : total === 0 ? 'Not Configured' : 'All Systems Operational'
|
||||||
|
const systemPercent = total === 0 ? 0 : Math.round((connected / total) * 100)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full grid-cols-4 gap-5">
|
<div className="grid w-full grid-cols-4 gap-5">
|
||||||
{/* System Status */}
|
{/* System Status */}
|
||||||
<div style={cardStyle} className="hover:!border-gold/20">
|
<div style={cardStyle} className="hover:!border-gold/20">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
<h3 style={labelStyle}>System Status</h3>
|
||||||
System Status
|
<p style={{ fontSize: '16px', fontWeight: 700, color: errored > 0 ? '#E74C3C' : '#2ECC71', lineHeight: 1.3 }}>{systemLabel}</p>
|
||||||
</h3>
|
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{connected} of {total} integrations connected</p>
|
||||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1.3 }}>All Systems</p>
|
|
||||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#2ECC71', lineHeight: 1.3 }}>Operational</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ProgressRing percentage={100} size={44} strokeWidth={3} />
|
<ProgressRing percentage={systemPercent} size={44} strokeWidth={3} />
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
|
|
||||||
<SparklineChart data={[100, 100, 99, 100, 100, 100, 98, 100, 100, 100, 100, 100]} color="#C8A434" height={20} />
|
|
||||||
<p style={{ fontSize: '9px', color: '#7A7D85', marginTop: '4px' }}>Last checked: 2m ago</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Infrastructure */}
|
{/* Infrastructure */}
|
||||||
<div style={cardStyle} className="hover:!border-gold/20">
|
<div style={cardStyle} className="hover:!border-gold/20">
|
||||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
<h3 style={labelStyle}>Infrastructure</h3>
|
||||||
Infrastructure
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Server size={16} style={{ color: '#C8A434' }} />
|
<Server size={16} style={{ color: '#C8A434' }} />
|
||||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>24</span>
|
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{resourceTotal}</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Total Resources</p>
|
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Resources from connected integrations</p>
|
||||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
|
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
|
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
|
||||||
<span style={{ color: '#7A7D85' }}>24 Running</span>
|
<span style={{ color: '#7A7D85' }}>{healthy} Healthy</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
|
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
|
||||||
<span style={{ color: '#7A7D85' }}>0 Warning</span>
|
<span style={{ color: '#7A7D85' }}>{warning} Warning</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
|
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
|
||||||
<span style={{ color: '#7A7D85' }}>0 Critical</span>
|
<span style={{ color: '#7A7D85' }}>{critical} Critical</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Security */}
|
{/* Integrations */}
|
||||||
<div style={cardStyle} className="hover:!border-gold/20">
|
<div style={cardStyle} className="hover:!border-gold/20">
|
||||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
<h3 style={labelStyle}>Integrations</h3>
|
||||||
Security
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield size={16} style={{ color: '#C8A434' }} />
|
<Plug size={16} style={{ color: '#C8A434' }} />
|
||||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>2</span>
|
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{connected}/{total}</span>
|
||||||
</div>
|
|
||||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Active Alerts</p>
|
|
||||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
|
|
||||||
<span style={{ color: '#7A7D85' }}>2 Low</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
|
|
||||||
<span style={{ color: '#7A7D85' }}>0 Medium</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
|
|
||||||
<span style={{ color: '#7A7D85' }}>0 High</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Connected services</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Network */}
|
{/* Bookmarks */}
|
||||||
<div style={cardStyle} className="hover:!border-gold/20">
|
<div style={cardStyle} className="hover:!border-gold/20">
|
||||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
<h3 style={labelStyle}>Bookmarks</h3>
|
||||||
Network
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Network size={16} style={{ color: '#C8A434' }} />
|
<BookMarked size={16} style={{ color: '#C8A434' }} />
|
||||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>98.7%</span>
|
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{bookmarks?.length ?? 0}</span>
|
||||||
</div>
|
|
||||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Network Uptime</p>
|
|
||||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
|
|
||||||
<SparklineChart data={[99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98]} color="#C8A434" height={24} filled />
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{favorites} favorited</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ export const api = {
|
||||||
updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) =>
|
updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) =>
|
||||||
apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: 'DELETE' }),
|
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: 'DELETE' }),
|
||||||
|
|
||||||
|
listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`),
|
||||||
|
listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Integration {
|
export interface Integration {
|
||||||
|
|
@ -98,3 +101,18 @@ export interface BookmarkCategory {
|
||||||
icon: string | null
|
icon: string | null
|
||||||
sort_order: number
|
sort_order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
source: string | null
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
name: string
|
||||||
|
status: 'healthy' | 'warning' | 'critical' | 'unknown'
|
||||||
|
detail?: string
|
||||||
|
integration: string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,12 @@
|
||||||
import { useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, LineChart, Line, XAxis } from 'recharts'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Plus, Server, Activity, AlertTriangle, DollarSign } from 'lucide-react'
|
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'
|
||||||
|
import { Plus, Server, Activity, AlertTriangle, CircleCheck } from 'lucide-react'
|
||||||
|
import { api, type Resource, type Integration, type Event } from '../lib/api'
|
||||||
|
|
||||||
const subTabs = ['Overview']
|
const subTabs = ['Overview']
|
||||||
const futureSubTabs = ['Network']
|
const futureSubTabs = ['Network']
|
||||||
|
|
||||||
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 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 nodes = [
|
|
||||||
{ name: 'web-frontend-lb', status: 'healthy' },
|
|
||||||
{ name: 'app-server-01', status: 'healthy' },
|
|
||||||
{ name: 'app-server-02', status: 'healthy' },
|
|
||||||
{ name: 'app-server-03', status: 'warning' },
|
|
||||||
{ name: 'db-primary-01', status: 'healthy' },
|
|
||||||
{ name: 'db-replica-02', status: 'healthy' },
|
|
||||||
{ name: 'cache-cluster-02', status: 'critical' },
|
|
||||||
{ name: 'batch-worker-04', status: 'healthy' },
|
|
||||||
{ name: 'storage-node-01', status: 'healthy' },
|
|
||||||
{ name: 'storage-node-02', status: 'healthy' },
|
|
||||||
{ name: 'monitoring-01', status: 'healthy' },
|
|
||||||
{ name: 'vpn-gateway', status: 'warning' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const nodeStatusColor: Record<string, string> = {
|
|
||||||
healthy: '#2ECC71',
|
|
||||||
warning: '#E67E22',
|
|
||||||
critical: '#E74C3C',
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
network: 45 + i * 0.3 + Math.cos(i / 2.5) * 2.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 = {
|
const cardBase: React.CSSProperties = {
|
||||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||||
|
|
@ -108,17 +59,21 @@ const cardDim: React.CSSProperties = {
|
||||||
backgroundColor: 'rgba(8, 8, 10, 0.45)',
|
backgroundColor: 'rgba(8, 8, 10, 0.45)',
|
||||||
}
|
}
|
||||||
|
|
||||||
const distributionData = [
|
const nodeStatusColor: Record<string, string> = {
|
||||||
{ name: 'Compute', value: 42, color: '#C8A434' },
|
healthy: '#2ECC71',
|
||||||
{ name: 'Storage', value: 28, color: '#E67E22' },
|
warning: '#E67E22',
|
||||||
{ name: 'Database', value: 18, color: '#2ECC71' },
|
critical: '#E74C3C',
|
||||||
{ name: 'Network', value: 12, color: '#7A7D85' },
|
unknown: '#7A7D85',
|
||||||
]
|
}
|
||||||
|
|
||||||
|
const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C']
|
||||||
|
|
||||||
function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) {
|
function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) {
|
||||||
|
const hasData = data.some((d) => d.value > 0)
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center gap-4">
|
<div className="flex flex-1 items-center gap-4">
|
||||||
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
|
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
|
||||||
|
{hasData && (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}>
|
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}>
|
||||||
|
|
@ -128,6 +83,7 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
|
||||||
</Pie>
|
</Pie>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
{centerLabel && (
|
{centerLabel && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span>
|
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span>
|
||||||
|
|
@ -138,8 +94,8 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
|
||||||
{data.map((entry) => (
|
{data.map((entry) => (
|
||||||
<div key={entry.name} className="flex items-center gap-2">
|
<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={{ 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: '#E8E6E0', width: '90px' }}>{entry.name}</span>
|
||||||
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}%</span>
|
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,6 +105,35 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
|
||||||
|
|
||||||
export default function Infrastructure() {
|
export default function Infrastructure() {
|
||||||
const [activeTab, setActiveTab] = useState('Overview')
|
const [activeTab, setActiveTab] = useState('Overview')
|
||||||
|
const [resources, setResources] = useState<Resource[] | null>(null)
|
||||||
|
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
|
||||||
|
const [events, setEvents] = useState<Event[] | null>(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.listResources().then(({ resources }) => setResources(resources))
|
||||||
|
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
|
||||||
|
api.listEvents(4).then(({ events }) => setEvents(events))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const healthy = resources?.filter((r) => r.status === 'healthy').length ?? 0
|
||||||
|
const warning = resources?.filter((r) => r.status === 'warning').length ?? 0
|
||||||
|
const critical = resources?.filter((r) => r.status === 'critical').length ?? 0
|
||||||
|
const total = resources?.length ?? 0
|
||||||
|
|
||||||
|
const statusCards = [
|
||||||
|
{ label: 'Total Resources', value: String(total), icon: Server, sub: `${integrations?.filter((i) => i.status === 'connected').length ?? 0} integrations connected` },
|
||||||
|
{ label: 'Healthy', value: String(healthy), icon: Activity, sub: total ? `${Math.round((healthy / total) * 100)}%` : '—', color: '#2ECC71' },
|
||||||
|
{ label: 'Warnings', value: String(warning), icon: AlertTriangle, sub: warning ? 'Needs attention' : 'None', color: '#E67E22' },
|
||||||
|
{ label: 'Critical', value: String(critical), icon: AlertTriangle, sub: critical ? 'Action required' : 'None', color: '#E74C3C' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const distributionData = useMemo(() => {
|
||||||
|
if (!resources) return []
|
||||||
|
const byIntegration = new Map<string, number>()
|
||||||
|
for (const r of resources) byIntegration.set(r.integration, (byIntegration.get(r.integration) ?? 0) + 1)
|
||||||
|
return Array.from(byIntegration.entries()).map(([name, value], i) => ({ name, value, color: integrationPalette[i % integrationPalette.length] }))
|
||||||
|
}, [resources])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -194,6 +179,7 @@ export default function Infrastructure() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
onClick={() => navigate('/settings')}
|
||||||
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
|
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
|
|
@ -212,7 +198,7 @@ export default function Infrastructure() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Cards */}
|
{/* Status Cards */}
|
||||||
<div className="grid w-full grid-cols-5 gap-5 shrink-0" style={{ marginTop: '8px', height: '110px' }}>
|
<div className="grid w-full grid-cols-4 gap-5 shrink-0" style={{ marginTop: '8px', height: '110px' }}>
|
||||||
{statusCards.map((card) => {
|
{statusCards.map((card) => {
|
||||||
const Icon = card.icon
|
const Icon = card.icon
|
||||||
return (
|
return (
|
||||||
|
|
@ -244,7 +230,11 @@ export default function Infrastructure() {
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<h3 style={sectionTitle}>Resource Distribution</h3>
|
<h3 style={sectionTitle}>Resource Distribution</h3>
|
||||||
<div className="flex flex-1 flex-col items-center justify-center">
|
<div className="flex flex-1 flex-col items-center justify-center">
|
||||||
|
{distributionData.length > 0 ? (
|
||||||
<Donut data={distributionData} />
|
<Donut data={distributionData} />
|
||||||
|
) : (
|
||||||
|
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>Connect an integration in Settings to see resource distribution.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -255,11 +245,12 @@ export default function Infrastructure() {
|
||||||
<div style={cardVignette} />
|
<div style={cardVignette} />
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<h3 style={sectionTitle}>Node Status</h3>
|
<h3 style={sectionTitle}>Node Status</h3>
|
||||||
|
{resources && resources.length > 0 ? (
|
||||||
<div className="grid flex-1 grid-cols-4 gap-3 content-center">
|
<div className="grid flex-1 grid-cols-4 gap-3 content-center">
|
||||||
{nodes.map((node) => (
|
{resources.map((node, i) => (
|
||||||
<div
|
<div
|
||||||
key={node.name}
|
key={i}
|
||||||
title={`${node.name}: ${node.status}`}
|
title={`${node.name}: ${node.detail ?? node.status}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
||||||
border: `1px solid ${nodeStatusColor[node.status]}33`,
|
border: `1px solid ${nodeStatusColor[node.status]}33`,
|
||||||
|
|
@ -278,6 +269,11 @@ export default function Infrastructure() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-1 items-center justify-center">
|
||||||
|
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>No resources reported yet. Connect Docker (or another supported integration) in Settings to populate this view.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -285,30 +281,26 @@ export default function Infrastructure() {
|
||||||
|
|
||||||
{/* Bottom Row */}
|
{/* Bottom Row */}
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div className="grid w-full grid-cols-[1.4fr_1fr_1fr] gap-6">
|
<div className="grid w-full grid-cols-2 gap-6">
|
||||||
{/* Resource Trend */}
|
{/* Integration Health */}
|
||||||
<div style={{ ...cardBase, height: 'auto', ...framedCard('/archnest-network-traffic-bg.png'), padding: '20px' }} 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="#3B82F6" 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} />
|
|
||||||
<Line type="monotone" dataKey="network" stroke="#8B5E3C" 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 style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||||
<div className="relative z-10 flex flex-col">
|
<div className="relative z-10 flex flex-col">
|
||||||
<h3 style={sectionTitle}>Cost Breakdown (MTD)</h3>
|
<h3 style={sectionTitle}>Integration Health</h3>
|
||||||
<Donut data={costData} centerLabel="$24.5K" />
|
{integrations && integrations.length > 0 ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{integrations.map((i) => (
|
||||||
|
<div key={i.id} className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-2" style={{ fontSize: '12px', color: '#E8E6E0' }}>
|
||||||
|
<CircleCheck size={13} style={{ color: i.status === 'connected' ? '#2ECC71' : i.status === 'error' ? '#E74C3C' : '#7A7D85' }} />
|
||||||
|
{i.name}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '11px', color: '#7A7D85' }}>{i.status}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No integrations added yet.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -316,17 +308,19 @@ export default function Infrastructure() {
|
||||||
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h3 style={sectionTitle}>Recent Activity</h3>
|
<h3 style={sectionTitle}>Recent Activity</h3>
|
||||||
|
{events && events.length > 0 ? (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{activities.map((item, i) => (
|
{events.map((item) => (
|
||||||
<div key={i} className="flex items-start justify-between gap-3">
|
<div key={item.id} className="flex items-start justify-between gap-3">
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<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: '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>
|
</div>
|
||||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -342,13 +336,14 @@ export default function Infrastructure() {
|
||||||
borderTop: '1px solid rgba(200,164,52,0.08)',
|
borderTop: '1px solid rgba(200,164,52,0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>AWS</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
<span>{total} Resources</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>{integrations?.filter((i) => i.status === 'connected').length ?? 0} Integrations Connected</span>
|
||||||
<span>18 AZs</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
|
{critical > 0 && (
|
||||||
<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 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: '#E74C3C' }}>{critical} Critical</span>
|
||||||
<span style={{ color: '#E67E22' }}>2 Alerts</span>
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue