auth-yes/replace_roles.py
google-labs-jules[bot] 4c7d8d4af7 feat(ui): decompose admin pages into reusable AdminTable and AdminModal components
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>
2026-08-26 00:34:29 +00:00

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!")