auth-yes/ui/components/AdminRolesPage.tsx
Tyler Gillispie 58d8e54ae5 fix(ui): wire AdminRolesScript and clean unused imports
- Replace inline script in AdminRolesPage with extracted AdminRolesScript component
- Remove unused JSX import aliases and format AdminUserDetailsPage
2026-08-26 11:43:52 -07:00

285 lines
11 KiB
TypeScript

import { AdminLayout } from "./AdminLayout.tsx";
import { RoleEditorDrawer } from "./admin/drawers/RoleEditorDrawer.tsx";
import { AdminRolesScript } from "./admin/AdminRolesScript.tsx";
export const AdminRolesPage = ({
roles,
apps,
}: {
roles: any[];
apps: any[];
}) => {
return (
<AdminLayout title="Role & Permission Catalog" currentPath="/admin/roles">
<div
id="status-banner"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
/>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
<div>
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
Role & Permission Catalog
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Manage global and application-scoped RBAC roles and permissions.
</p>
</div>
<button
type="button"
class="btn-primary"
style="min-height: 40px;"
onclick="openCreateRoleDrawer()"
>
+ Create Custom Role
</button>
</div>
{/* Create / Edit Role Drawer */}
<RoleEditorDrawer apps={apps} />
<div class="card">
{/* Instant Search and Scope Filters */}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem;">
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; flex: 1;">
{/* Search */}
<div style="position: relative; min-width: 200px; max-width: 300px; width: 100%;">
<input
type="text"
id="roleSearchInput"
placeholder="Search roles..."
oninput="filterRoles()"
style="width: 100%; padding: 0.45rem 0.85rem 0.45rem 2.1rem; font-size: 0.85rem;"
/>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
style="position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none;"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
{/* Scope dropdown */}
<div style="display: flex; gap: 0.4rem; align-items: center;">
<label style="font-weight: 600; font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap;">
Scope:
</label>
<select
id="filterScopeSelect"
onchange="filterRoles()"
style="padding: 0.4rem 0.75rem; font-size: 0.85rem;"
>
<option value="all">All Roles</option>
<option value="global">Global (Shared) Only</option>
{apps.map((app) => (
<option value={app.id}>
{app.name} Only
</option>
))}
</select>
</div>
</div>
<span
id="roleCountDisplay"
style="font-size: 0.85rem; color: var(--text-muted);"
>
Showing {roles.length} roles
</span>
</div>
{/* Desktop Table View (≥ 768px) */}
<div class="table-container desktop-only" style="display: none;">
<table id="rolesTable">
<thead>
<tr>
<th>Role Identifier</th>
<th>Scope</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{roles.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No roles found.
</td>
</tr>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
{r.name}
</strong>
</td>
<td>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.35rem;">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<span style="color: var(--text-muted); font-size: 0.8rem; font-style: italic;">
System Core
</span>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Mobile Adaptive Cards View (< 768px) */}
<div
id="rolesMobileDeck"
class="mobile-only"
style="display: flex; flex-direction: column; gap: 0.75rem;"
>
{roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<div
class="card role-card"
key={r.id}
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="font-family: monospace; font-size: 1rem; color: var(--text-primary);">
{r.name}
</strong>
{isGlobal
? <span class="badge badge-info">Global</span>
: (
<span class="badge badge-warning">
{r.app_name || "App Scoped"}
</span>
)}
</div>
<p style="margin: 0 0 0.75rem 0; font-size: 0.85rem; color: var(--text-secondary);">
{r.description || "No description provided."}
</p>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
<button
type="button"
class="btn-danger"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<div style="font-size: 0.8rem; color: var(--text-muted); font-style: italic;">
Protected System Core Role
</div>
)}
</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>
<AdminRolesScript />
</AdminLayout>
);
};