Redesign beta Magent UI
Magent CI/CD / verify (push) Successful in 11m8s
Magent CI/CD / deploy-prod (push) Has been skipped
Magent CI/CD / deploy-beta (push) Successful in 18s

This commit is contained in:
2026-06-21 11:41:38 +12:00
parent e36da13264
commit e6b4f99ea7
12 changed files with 1891 additions and 40 deletions
+240 -6
View File
@@ -1,24 +1,258 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
import AdminShell from '../ui/AdminShell'
type ServiceState = {
name: string
status: string
message?: string
}
type RecentRequest = {
id: number
title?: string | null
year?: number | null
statusLabel?: string | null
requestedBy?: string | null
createdAt?: string | null
}
type PortalOverview = {
overview?: {
total_items?: number
total_comments?: number
by_kind?: Record<string, number>
by_status?: Record<string, number>
}
my_items?: number
}
const formatDateTime = (value?: string | null) => {
if (!value) return 'Unknown'
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return value
return date.toLocaleString()
}
const normalizeRecent = (items: any[]): RecentRequest[] =>
items
.filter((item) => item?.id)
.map((item) => ({
id: Number(item.id),
title: item.title ?? null,
year: item.year ?? null,
statusLabel: item.statusLabel ?? null,
requestedBy: item.requestedBy ?? null,
createdAt: item.createdAt ?? null,
}))
export default function AdminLandingPage() {
const router = useRouter()
const [services, setServices] = useState<ServiceState[]>([])
const [serviceOverall, setServiceOverall] = useState('unknown')
const [recent, setRecent] = useState<RecentRequest[]>([])
const [portalOverview, setPortalOverview] = useState<PortalOverview | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!getToken()) {
router.push('/login')
return
}
const load = async () => {
setLoading(true)
setError(null)
try {
const baseUrl = getApiBase()
const [meResponse, serviceResponse, recentResponse, overviewResponse] = await Promise.all([
authFetch(`${baseUrl}/auth/me`),
authFetch(`${baseUrl}/status/services`),
authFetch(`${baseUrl}/requests/recent?take=8&days=0`),
authFetch(`${baseUrl}/portal/overview`),
])
if (meResponse.status === 401) {
clearToken()
router.push('/login')
return
}
if (meResponse.status === 403) {
router.push('/')
return
}
const me = await meResponse.json()
if (me?.role !== 'admin') {
router.push('/')
return
}
if (serviceResponse.ok) {
const data = await serviceResponse.json()
setServiceOverall(data?.overall ?? 'unknown')
setServices(Array.isArray(data?.services) ? data.services : [])
}
if (recentResponse.ok) {
const data = await recentResponse.json()
setRecent(Array.isArray(data?.results) ? normalizeRecent(data.results) : [])
}
if (overviewResponse.ok) {
const data = await overviewResponse.json()
setPortalOverview(data)
}
} catch (err) {
console.error(err)
setError('Unable to load the operations dashboard.')
} finally {
setLoading(false)
}
}
void load()
}, [router])
const serviceCounts = useMemo(() => {
const up = services.filter((service) => service.status === 'up').length
const down = services.filter((service) => service.status === 'down').length
const degraded = services.filter((service) => service.status === 'degraded').length
const notConfigured = services.filter((service) => service.status === 'not_configured').length
return { up, down, degraded, notConfigured, total: services.length }
}, [services])
const issueCount = Number(portalOverview?.overview?.by_kind?.issue ?? 0)
const requestItemCount = Number(portalOverview?.overview?.by_kind?.request ?? 0)
const commentCount = Number(portalOverview?.overview?.total_comments ?? 0)
const rail = (
<div className="admin-rail-stack">
<div className="admin-rail-card">
<span className="admin-rail-eyebrow">Service ecosystem</span>
<div className="service-ecosystem">
{services.length === 0 ? (
<div className="status-banner">Service status is not available yet.</div>
) : (
services.map((service) => (
<a
key={service.name}
className="service-row"
href={`/admin/${service.name.toLowerCase().replace(/[^a-z0-9]/g, '')}`}
>
<span className={`system-dot system-dot-${service.status}`} />
<span>
<strong>{service.name}</strong>
<small>{service.message ?? 'No message reported'}</small>
</span>
<span className={`small-pill system-pill-${service.status}`}>{service.status}</span>
</a>
))
)}
</div>
</div>
<div className="admin-rail-card">
<span className="admin-rail-eyebrow">Quick actions</span>
<div className="quick-action-grid">
<a href="/admin/requests-all">Review requests</a>
<a href="/admin/issues">Manage issues</a>
<a href="/users">User directory</a>
<a href="/admin/logs">Activity log</a>
</div>
</div>
</div>
)
return (
<AdminShell
title="Settings"
subtitle="Choose what you want to manage."
title="Operations Center"
subtitle="Live Magent controls, request movement, issue intake, and service health."
rail={rail}
actions={
<button type="button" onClick={() => router.push('/')}>
Back to requests
View health
</button>
}
>
<section className="admin-section">
<div className="status-banner">
Pick a section from the left. Each page explains what it does and how it helps.
{loading ? <div className="status-banner">Loading operations dashboard...</div> : null}
{error ? <div className="error-banner">{error}</div> : null}
<section className="ops-metric-grid">
<div className="ops-metric-card">
<span className="section-kicker">Services online</span>
<strong>
{serviceCounts.up}/{serviceCounts.total || 0}
</strong>
<p>{serviceOverall === 'up' ? 'All configured services are responding.' : 'Some services need review.'}</p>
</div>
<div className="ops-metric-card">
<span className="section-kicker">Recent requests</span>
<strong>{recent.length}</strong>
<p>Loaded from the live request cache.</p>
</div>
<div className="ops-metric-card">
<span className="section-kicker">Open issue items</span>
<strong>{issueCount}</strong>
<p>{commentCount} portal comments recorded.</p>
</div>
<div className="ops-metric-card">
<span className="section-kicker">Portal requests</span>
<strong>{requestItemCount}</strong>
<p>Tracked in the dedicated request workflow.</p>
</div>
</section>
<section className="admin-zone">
<div className="section-header">
<div>
<h2>Recent activity</h2>
<p className="section-subtitle">Live request cache entries, newest first.</p>
</div>
</div>
{recent.length === 0 ? (
<div className="status-banner">No recent requests were returned.</div>
) : (
<div className="admin-table dashboard-activity-table">
<div className="admin-table-head">
<span>Request</span>
<span>Status</span>
<span>User</span>
<span>Created</span>
</div>
{recent.map((row) => (
<button
key={row.id}
type="button"
className="admin-table-row"
onClick={() => router.push(`/requests/${row.id}`)}
>
<span>
{row.title || `Request #${row.id}`}
{row.year ? ` (${row.year})` : ''}
</span>
<span>{row.statusLabel || 'Unknown'}</span>
<span>{row.requestedBy || 'Unknown'}</span>
<span>{formatDateTime(row.createdAt)}</span>
</button>
))}
</div>
)}
</section>
<section className="admin-zone">
<div className="section-header">
<div>
<h2>Attention states</h2>
<p className="section-subtitle">Service states that affect request processing.</p>
</div>
</div>
<div className="ops-status-strip">
<span>{serviceCounts.down} down</span>
<span>{serviceCounts.degraded} degraded</span>
<span>{serviceCounts.notConfigured} not configured</span>
</div>
</section>
</AdminShell>