Redesign beta Magent UI
This commit is contained in:
+240
-6
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user