Refactored AdminInvitesPage, AdminUserDetailsPage, AdminRolesPage, and AdminAppsPage to use the new pure Hono SSR JSX stateless components. Fixed missing import definitions in AdminRolesPage. Moved task file to complete state. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
569 lines
23 KiB
Python
569 lines
23 KiB
Python
import re
|
|
|
|
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
|
content = f.read()
|
|
|
|
import_str = """import { AdminLayout } from "./AdminLayout.tsx";"""
|
|
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";""")
|
|
|
|
role_form = """ <div
|
|
id="roleFormCard"
|
|
class="card"
|
|
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
|
|
>
|
|
<h3
|
|
id="roleFormTitle"
|
|
style="margin: 0 0 0.5rem 0; color: var(--text-primary);"
|
|
>
|
|
Create New Role
|
|
</h3>
|
|
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
|
Define a global shared role or an application-scoped custom grant.
|
|
</p>
|
|
|
|
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
|
<input type="hidden" id="editRoleId" value="" />
|
|
|
|
<div
|
|
id="scopeSelectContainer"
|
|
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
|
>
|
|
<div>
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Scope (Applicability) *
|
|
</label>
|
|
<select
|
|
id="roleScope"
|
|
onchange="handleScopeChange()"
|
|
style="width: 100%;"
|
|
>
|
|
<option value="global">
|
|
Global (Shared across ALL applications)
|
|
</option>
|
|
<option value="app_specific">
|
|
Application-Specific (Scoped to single app)
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="appSelectContainer" style="display: none;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Target Application *
|
|
</label>
|
|
<select
|
|
id="roleAppId"
|
|
style="width: 100%;"
|
|
>
|
|
{apps.map((app) => (
|
|
<option value={app.id}>
|
|
{app.name} ({app.spiffe_id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Role Name (Identifier) *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="roleName"
|
|
placeholder="e.g. read_only, viewer, admin"
|
|
required
|
|
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
|
|
/>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Description
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="roleDescription"
|
|
placeholder="e.g. Read-only access to basic features"
|
|
style="width: 100%;"
|
|
/>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.75rem;">
|
|
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
|
Save Role
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
onclick="closeRoleForm()"
|
|
style="min-height: 42px;"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>"""
|
|
|
|
role_form_replacement = """ <AdminModal
|
|
id="roleFormModal"
|
|
title="Create New Role"
|
|
onClose="closeRoleForm()"
|
|
>
|
|
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
|
Define a global shared role or an application-scoped custom grant.
|
|
</p>
|
|
|
|
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
|
<input type="hidden" id="editRoleId" value="" />
|
|
|
|
<div
|
|
id="scopeSelectContainer"
|
|
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
|
>
|
|
<div>
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Scope (Applicability) *
|
|
</label>
|
|
<select
|
|
id="roleScope"
|
|
onchange="handleScopeChange()"
|
|
style="width: 100%;"
|
|
>
|
|
<option value="global">
|
|
Global (Shared across ALL applications)
|
|
</option>
|
|
<option value="app_specific">
|
|
Application-Specific (Scoped to single app)
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="appSelectContainer" style="display: none;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Target Application *
|
|
</label>
|
|
<select
|
|
id="roleAppId"
|
|
style="width: 100%;"
|
|
>
|
|
{apps.map((app) => (
|
|
<option value={app.id}>
|
|
{app.name} ({app.spiffe_id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Role Name (Identifier) *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="roleName"
|
|
placeholder="e.g. read_only, viewer, admin"
|
|
required
|
|
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
|
|
/>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
|
Description
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="roleDescription"
|
|
placeholder="e.g. Read-only access to basic features"
|
|
style="width: 100%;"
|
|
/>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.75rem;">
|
|
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
|
Save Role
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
onclick="closeRoleForm()"
|
|
style="min-height: 42px;"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</AdminModal>"""
|
|
|
|
content = content.replace(role_form, role_form_replacement)
|
|
|
|
roles_table_desktop_mobile = """ <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;
|
|
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>
|
|
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
|
|
{r.name}
|
|
</code>
|
|
</td>
|
|
<td>
|
|
{isGlobal
|
|
? <span class="badge badge-info">Global</span>
|
|
: (
|
|
<span class="badge badge-warning">
|
|
{r.app_name || "App Scoped"}
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td style="color: var(--text-secondary); max-width: 250px; 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>
|
|
<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={`openEditRoleForm(${
|
|
JSON.stringify(JSON.stringify(r))
|
|
})`}
|
|
>
|
|
Edit
|
|
</button>
|
|
{r.name !== "admin" && (
|
|
<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>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Mobile View (< 768px) */}
|
|
<div
|
|
id="rolesMobileDeck"
|
|
class="mobile-only"
|
|
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
|
>
|
|
{roles.length === 0
|
|
? (
|
|
<div class="card" style="text-align: center; padding: 2rem;">
|
|
<p style="color: var(--text-muted); margin: 0;">
|
|
No roles found.
|
|
</p>
|
|
</div>
|
|
)
|
|
: (
|
|
roles.map((r) => {
|
|
const isGlobal = !r.app_id;
|
|
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>
|
|
|
|
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
|
|
<p style="margin: 0 0 0.25rem 0;">
|
|
{r.description || "No description provided."}
|
|
</p>
|
|
<small style="color: var(--text-muted);">
|
|
Created: {new Date(r.created_at).toLocaleDateString()}
|
|
</small>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`openEditRoleForm(${
|
|
JSON.stringify(JSON.stringify(r))
|
|
})`}
|
|
>
|
|
Edit Role
|
|
</button>
|
|
{r.name !== "admin" && (
|
|
<button
|
|
type="button"
|
|
class="btn-danger"
|
|
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
|
>
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>"""
|
|
|
|
roles_table_replacement = """ <AdminTable
|
|
id="rolesTable"
|
|
headers={[
|
|
"Role Identifier",
|
|
"Scope",
|
|
"Description",
|
|
"Created",
|
|
"Actions",
|
|
]}
|
|
isEmpty={roles.length === 0}
|
|
emptyState="No roles found."
|
|
desktopRows={roles.map((r) => {
|
|
const isGlobal = !r.app_id;
|
|
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>
|
|
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
|
|
{r.name}
|
|
</code>
|
|
</td>
|
|
<td>
|
|
{isGlobal
|
|
? <span class="badge badge-info">Global</span>
|
|
: (
|
|
<span class="badge badge-warning">
|
|
{r.app_name || "App Scoped"}
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td style="color: var(--text-secondary); max-width: 250px; 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>
|
|
<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={`openEditRoleForm(${
|
|
JSON.stringify(JSON.stringify(r))
|
|
})`}
|
|
>
|
|
Edit
|
|
</button>
|
|
{r.name !== "admin" && (
|
|
<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>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
mobileCards={roles.map((r) => {
|
|
const isGlobal = !r.app_id;
|
|
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>
|
|
|
|
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
|
|
<p style="margin: 0 0 0.25rem 0;">
|
|
{r.description || "No description provided."}
|
|
</p>
|
|
<small style="color: var(--text-muted);">
|
|
Created: {new Date(r.created_at).toLocaleDateString()}
|
|
</small>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button
|
|
type="button"
|
|
class="btn-outline"
|
|
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`openEditRoleForm(${
|
|
JSON.stringify(JSON.stringify(r))
|
|
})`}
|
|
>
|
|
Edit Role
|
|
</button>
|
|
{r.name !== "admin" && (
|
|
<button
|
|
type="button"
|
|
class="btn-danger"
|
|
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
|
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
|
>
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
/>"""
|
|
|
|
content = content.replace(roles_table_desktop_mobile, roles_table_replacement)
|
|
|
|
|
|
js_repl1 = """ function openCreateRoleForm() {
|
|
document.getElementById('editRoleId').value = '';
|
|
document.getElementById('roleFormTitle').textContent = 'Create New Role';
|
|
document.getElementById('roleScope').value = 'global';
|
|
document.getElementById('roleScope').disabled = false;
|
|
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
|
|
document.getElementById('roleName').value = '';
|
|
document.getElementById('roleName').readOnly = false;
|
|
document.getElementById('roleDescription').value = '';
|
|
handleScopeChange();
|
|
document.getElementById('roleFormCard').style.display = 'block';
|
|
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
|
|
}"""
|
|
|
|
js_repl1_new = """ function openCreateRoleForm() {
|
|
document.getElementById('editRoleId').value = '';
|
|
document.querySelector('#roleFormModal h3').textContent = 'Create New Role';
|
|
document.getElementById('roleScope').value = 'global';
|
|
document.getElementById('roleScope').disabled = false;
|
|
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
|
|
document.getElementById('roleName').value = '';
|
|
document.getElementById('roleName').readOnly = false;
|
|
document.getElementById('roleDescription').value = '';
|
|
handleScopeChange();
|
|
document.getElementById('roleFormModal').style.display = 'flex';
|
|
}"""
|
|
content = content.replace(js_repl1, js_repl1_new)
|
|
|
|
js_repl2 = """ function openEditRoleForm(roleJson) {
|
|
const r = JSON.parse(roleJson);
|
|
document.getElementById('editRoleId').value = r.id;
|
|
document.getElementById('roleFormTitle').textContent = 'Edit Role: ' + r.name;
|
|
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
|
|
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
|
|
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
|
|
document.getElementById('roleName').value = r.name;
|
|
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
|
|
document.getElementById('roleDescription').value = r.description || '';
|
|
handleScopeChange();
|
|
document.getElementById('roleFormCard').style.display = 'block';
|
|
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
|
|
}"""
|
|
|
|
js_repl2_new = """ function openEditRoleForm(roleJson) {
|
|
const r = JSON.parse(roleJson);
|
|
document.getElementById('editRoleId').value = r.id;
|
|
document.querySelector('#roleFormModal h3').textContent = 'Edit Role: ' + r.name;
|
|
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
|
|
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
|
|
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
|
|
document.getElementById('roleName').value = r.name;
|
|
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
|
|
document.getElementById('roleDescription').value = r.description || '';
|
|
handleScopeChange();
|
|
document.getElementById('roleFormModal').style.display = 'flex';
|
|
}"""
|
|
content = content.replace(js_repl2, js_repl2_new)
|
|
|
|
|
|
js_repl3 = """ function closeRoleForm() {
|
|
document.getElementById('roleFormCard').style.display = 'none';
|
|
}"""
|
|
|
|
js_repl3_new = """ function closeRoleForm() {
|
|
document.getElementById('roleFormModal').style.display = 'none';
|
|
}"""
|
|
content = content.replace(js_repl3, js_repl3_new)
|
|
|
|
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
|
f.write(content)
|
|
print("Replaced Roles table!")
|