feat(ui): adapt admin management screens into responsive mobile cards and timeline feed

This commit is contained in:
Tyler Gillispie 2026-08-24 22:20:47 -07:00
parent 7ab1405459
commit 509e6019b0
3 changed files with 309 additions and 75 deletions

View File

@ -9,12 +9,21 @@ export const AdminUsersPage = ({
<AdminLayout title="Manage Users" currentPath="/admin/users"> <AdminLayout title="Manage Users" currentPath="/admin/users">
<div <div
id="status-banner" id="status-banner"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: 4px; font-size: 0.9rem;" style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
/> />
<div class="card"> <div style="margin-bottom: 1.5rem;">
<h2>User Management</h2> <h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
<p>Review and activate pending users.</p> User Management
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Manage identities, activate pending registrations, and inspect role
grants.
</p>
</div>
{/* Desktop Table View (≥ 768px) */}
<div class="card desktop-only" style="display: none;">
<div class="table-container"> <div class="table-container">
<table> <table>
<thead> <thead>
@ -26,58 +35,168 @@ export const AdminUsersPage = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map((user) => ( {users.map((user) => {
<tr key={user.id}> const statusClass = user.account_status === "active"
<td>{user.username}</td> ? "badge-success"
<td>{user.display_name || "-"}</td> : user.account_status === "suspended"
<td> ? "badge-danger"
<span class={`badge badge-${user.account_status}`}> : "badge-warning";
{user.account_status}
</span> return (
</td> <tr key={user.id}>
<td> <td>
<a <strong style="color: var(--text-primary);">
href={`/admin/users/${user.id}`} {user.username}
class="btn-action" </strong>
style="text-decoration: none;" </td>
> <td style="color: var(--text-secondary);">
Manage {user.display_name || "-"}
</a> </td>
{user.account_status === "pending" && ( <td>
<button <span class={`badge ${statusClass}`}>
type="button" {user.account_status}
class="btn-action btn-success" </span>
onclick={`updateStatus('${user.id}', 'active')`} </td>
> <td>
Activate <div style="display: flex; gap: 0.35rem;">
</button> <a
)} href={`/admin/users/${user.id}`}
{user.account_status === "active" && ( class="btn-outline"
<button style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px; text-decoration: none;"
type="button" >
class="btn-action btn-warning" Manage
onclick={`updateStatus('${user.id}', 'suspended')`} </a>
> {user.account_status === "pending" && (
Suspend <button
</button> type="button"
)} class="btn-primary"
{user.account_status === "suspended" && ( style="background: var(--success); border-color: var(--success); padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
<button onclick={`updateStatus('${user.id}', 'active')`}
type="button" >
class="btn-action btn-success" Activate
onclick={`updateStatus('${user.id}', 'active')`} </button>
> )}
Re-Activate {user.account_status === "active" && (
</button> <button
)} type="button"
</td> class="btn-danger"
</tr> style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
))} onclick={`updateStatus('${user.id}', 'suspended')`}
>
Suspend
</button>
)}
{user.account_status === "suspended" && (
<button
type="button"
class="btn-primary"
style="background: var(--success); border-color: var(--success); padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`updateStatus('${user.id}', 'active')`}
>
Re-Activate
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{/* Mobile Card Deck (< 768px) */}
<div
class="mobile-only"
style="display: flex; flex-direction: column; gap: 1rem;"
>
{users.map((user) => {
const statusClass = user.account_status === "active"
? "badge-success"
: user.account_status === "suspended"
? "badge-danger"
: "badge-warning";
return (
<div class="card" key={user.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.65rem;">
<div style="display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; background: var(--primary-light); color: var(--primary); border-radius: var(--radius-md); font-weight: 700; font-size: 1rem;">
{user.username.charAt(0).toUpperCase()}
</div>
<div>
<h3 style="margin: 0; font-size: 1.05rem; color: var(--text-primary);">
{user.username}
</h3>
<span style="font-size: 0.8rem; color: var(--text-muted);">
{user.display_name || "No display name"}
</span>
</div>
</div>
<span class={`badge ${statusClass}`}>
{user.account_status}
</span>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<a
href={`/admin/users/${user.id}`}
class="btn-outline"
style="flex: 1; text-decoration: none; justify-content: center; min-height: 40px; font-size: 0.85rem;"
>
Manage User
</a>
{user.account_status === "pending" && (
<button
type="button"
class="btn-primary"
style="flex: 1; background: var(--success); border-color: var(--success); justify-content: center; min-height: 40px; font-size: 0.85rem;"
onclick={`updateStatus('${user.id}', 'active')`}
>
Activate
</button>
)}
{user.account_status === "active" && (
<button
type="button"
class="btn-danger"
style="flex: 1; justify-content: center; min-height: 40px; font-size: 0.85rem;"
onclick={`updateStatus('${user.id}', 'suspended')`}
>
Suspend
</button>
)}
{user.account_status === "suspended" && (
<button
type="button"
class="btn-primary"
style="flex: 1; background: var(--success); border-color: var(--success); justify-content: center; min-height: 40px; font-size: 0.85rem;"
onclick={`updateStatus('${user.id}', 'active')`}
>
Re-Activate
</button>
)}
</div>
</div>
);
})}
</div>
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
}
`}
</style>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
@ -85,9 +204,9 @@ export const AdminUsersPage = ({
const banner = document.getElementById('status-banner'); const banner = document.getElementById('status-banner');
banner.textContent = msg; banner.textContent = msg;
banner.style.display = 'block'; banner.style.display = 'block';
banner.style.background = isError ? '#f8d7da' : '#d1e7dd'; banner.style.background = isError ? 'var(--danger-bg)' : 'var(--success-bg)';
banner.style.color = isError ? '#842029' : '#0f5132'; banner.style.color = isError ? 'var(--danger-text)' : 'var(--success-text)';
banner.style.border = isError ? '1px solid #f5c2c7' : '1px solid #badbcc'; banner.style.border = isError ? '1px solid var(--danger-border)' : '1px solid var(--success-border)';
setTimeout(() => { banner.style.display = 'none'; }, 6000); setTimeout(() => { banner.style.display = 'none'; }, 6000);
} }

View File

@ -6,40 +6,80 @@ export const AuditLogPage = ({
logs: any[]; logs: any[];
}) => { }) => {
return ( return (
<AdminLayout title="System Audit Logs" currentPath="/admin/audit-logs"> <AdminLayout title="Audit Logs" currentPath="/admin/audit-logs">
<div class="card"> <div style="margin-bottom: 1.5rem;">
<h2>System Audit Logs</h2> <h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
Immutable Audit Ledger
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Cryptographically chained Merkle audit trail for authentication,
authorization, and administrative events.
</p>
</div>
{/* Desktop Table (≥ 768px) */}
<div class="card desktop-only" style="display: none;">
<div class="table-container"> <div class="table-container">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Timestamp</th> <th>Timestamp</th>
<th>Action</th> <th>Event Action</th>
<th>User</th> <th>User / Subject</th>
<th>Resource</th> <th>Resource</th>
<th>IP Address</th> <th>IP Address</th>
<th>Details</th> <th>Details</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{logs.map((log) => ( {logs.map((log) => {
<tr> const isFail = log.action.includes("fail") ||
<td>{new Date(log.created_at).toLocaleString()}</td> log.action.includes("denied");
<td> const isSuccess = log.action.includes("success") ||
<strong>{log.action}</strong> log.action.includes("create") ||
</td> log.action.includes("activate");
<td>{log.user || "System"}</td> const badgeClass = isFail
<td>{log.resource || "-"}</td> ? "badge-danger"
<td>{log.ip_address || "-"}</td> : isSuccess
<td> ? "badge-success"
<pre>{log.details ? JSON.stringify(log.details, null, 2) : "{}"}</pre> : "badge-info";
</td>
</tr> return (
))} <tr key={log.id}>
<td style="font-size: 0.8rem; color: var(--text-muted); white-space: nowrap;">
{new Date(log.created_at).toLocaleString()}
</td>
<td>
<span class={`badge ${badgeClass}`}>
{log.action}
</span>
</td>
<td>
<strong style="color: var(--text-primary);">
{log.user || "System"}
</strong>
</td>
<td style="color: var(--text-secondary);">
{log.resource || "-"}
</td>
<td style="font-family: monospace; font-size: 0.85rem;">
{log.ip_address || "-"}
</td>
<td>
<pre style="margin: 0; font-family: monospace; font-size: 0.75rem; background: var(--surface-muted); padding: 0.35rem 0.6rem; border-radius: var(--radius-sm); max-width: 300px; overflow-x: auto;">
{log.details ? JSON.stringify(log.details) : "{}"}
</pre>
</td>
</tr>
);
})}
{logs.length === 0 && ( {logs.length === 0 && (
<tr> <tr>
<td colSpan={6} style={{ textAlign: "center" }}> <td
No audit logs found. colSpan={6}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No audit records recorded yet.
</td> </td>
</tr> </tr>
)} )}
@ -47,6 +87,81 @@ export const AuditLogPage = ({
</table> </table>
</div> </div>
</div> </div>
{/* Mobile Feed (< 768px) */}
<div
class="mobile-only"
style="display: flex; flex-direction: column; gap: 0.75rem;"
>
{logs.map((log) => {
const isFail = log.action.includes("fail") ||
log.action.includes("denied");
const isSuccess = log.action.includes("success") ||
log.action.includes("create") || log.action.includes("activate");
const badgeClass = isFail
? "badge-danger"
: isSuccess
? "badge-success"
: "badge-info";
return (
<div
class="card"
key={log.id}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<span class={`badge ${badgeClass}`}>
{log.action}
</span>
<span style="font-size: 0.75rem; color: var(--text-muted);">
{new Date(log.created_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</span>
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
<div>
<strong>User:</strong> {log.user || "System"}
</div>
<div>
<strong>IP:</strong>{" "}
<span style="font-family: monospace;">
{log.ip_address || "Internal"}
</span>
</div>
</div>
{log.details && (
<details style="font-size: 0.8rem; color: var(--text-muted);">
<summary style="cursor: pointer; font-weight: 600;">
View Event Payload
</summary>
<pre style="margin-top: 0.5rem; background: var(--surface-muted); padding: 0.5rem; border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow-x: auto;">
{JSON.stringify(log.details, null, 2)}
</pre>
</details>
)}
</div>
);
})}
</div>
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
}
`}
</style>
</AdminLayout> </AdminLayout>
); );
}; };