feat(admin): add manage and edit pages for runtime configs
This commit is contained in:
parent
e5ce1ef1b3
commit
562ad4f8de
9 changed files with 499 additions and 15 deletions
43
src/app.css
43
src/app.css
|
|
@ -186,3 +186,46 @@ body {
|
|||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.list-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
border: 1px solid #dbe1ec;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.list-item h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.list-item p {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.list-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { A, useLocation } from '@solidjs/router';
|
|||
|
||||
const links = [
|
||||
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
|
||||
{ href: '/admin/runtime-roles', label: 'Manage Roles' },
|
||||
{ href: '/admin/role-ui-configs/new', label: 'Create Dashboard' },
|
||||
{ href: '/admin/role-ui-configs', label: 'Manage Dashboards' },
|
||||
{ href: '/admin/onboarding-schemas/new', label: 'Create Onboarding Flow' },
|
||||
{ href: '/admin/onboarding-schemas', label: 'Manage Onboarding Flows' },
|
||||
];
|
||||
|
||||
export default function AdminSidebar() {
|
||||
|
|
@ -14,11 +17,20 @@ export default function AdminSidebar() {
|
|||
<div class="brand">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" />
|
||||
</div>
|
||||
{links.map((item) => (
|
||||
<A href={item.href} class={`nav-item ${location.pathname === item.href ? 'active' : ''}`}>
|
||||
{item.label}
|
||||
</A>
|
||||
))}
|
||||
{links.map((item) => {
|
||||
const isActive =
|
||||
location.pathname === item.href ||
|
||||
(item.href !== '/admin/runtime-roles' &&
|
||||
item.href !== '/admin/role-ui-configs' &&
|
||||
item.href !== '/admin/onboarding-schemas' &&
|
||||
location.pathname.startsWith(item.href.replace('/new', '')));
|
||||
|
||||
return (
|
||||
<A href={item.href} class={`nav-item ${isActive ? 'active' : ''}`}>
|
||||
{item.label}
|
||||
</A>
|
||||
);
|
||||
})}
|
||||
<p class="notice">Same UI flow, simpler builder experience.</p>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,17 +4,30 @@ export type RuntimeRecordStatus = 'draft' | 'published';
|
|||
const KEY = 'nxtgauge_admin_runtime_builder_v1';
|
||||
|
||||
type RuntimeStore = {
|
||||
role: Record<string, unknown>;
|
||||
dashboard: Record<string, unknown>;
|
||||
onboarding: Record<string, unknown>;
|
||||
role: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
dashboard: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
onboarding: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
};
|
||||
|
||||
export type RuntimeStoredRecord<T> = {
|
||||
status: RuntimeRecordStatus;
|
||||
updatedAt: string;
|
||||
payload: T;
|
||||
};
|
||||
|
||||
export type RuntimeListItem<T> = RuntimeStoredRecord<T> & {
|
||||
key: string;
|
||||
};
|
||||
|
||||
function emptyStore(): RuntimeStore {
|
||||
return { role: {}, dashboard: {}, onboarding: {} };
|
||||
}
|
||||
|
||||
function readStore(): RuntimeStore {
|
||||
if (typeof window === 'undefined') {
|
||||
return { role: {}, dashboard: {}, onboarding: {} };
|
||||
}
|
||||
if (typeof window === 'undefined') return emptyStore();
|
||||
const raw = window.localStorage.getItem(KEY);
|
||||
if (!raw) return { role: {}, dashboard: {}, onboarding: {} };
|
||||
if (!raw) return emptyStore();
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<RuntimeStore>;
|
||||
return {
|
||||
|
|
@ -23,7 +36,7 @@ function readStore(): RuntimeStore {
|
|||
onboarding: parsed.onboarding || {},
|
||||
};
|
||||
} catch {
|
||||
return { role: {}, dashboard: {}, onboarding: {} };
|
||||
return emptyStore();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,9 +46,11 @@ function writeStore(store: RuntimeStore) {
|
|||
}
|
||||
|
||||
export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) {
|
||||
const normalizedKey = key.trim();
|
||||
if (!normalizedKey) return;
|
||||
|
||||
const store = readStore();
|
||||
const bucket = store[type];
|
||||
bucket[key] = {
|
||||
store[type][normalizedKey] = {
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
payload,
|
||||
|
|
@ -43,3 +58,22 @@ export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload:
|
|||
writeStore(store);
|
||||
}
|
||||
|
||||
export function listRuntimeConfigs<T>(type: RuntimeRecordType): Array<RuntimeListItem<T>> {
|
||||
const store = readStore();
|
||||
return Object.entries(store[type])
|
||||
.map(([key, value]) => ({ key, ...(value as RuntimeStoredRecord<T>) }))
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
}
|
||||
|
||||
export function getRuntimeConfig<T>(type: RuntimeRecordType, key: string): RuntimeStoredRecord<T> | null {
|
||||
const record = readStore()[type][key];
|
||||
return (record as RuntimeStoredRecord<T>) || null;
|
||||
}
|
||||
|
||||
export function deleteRuntimeConfig(type: RuntimeRecordType, key: string): boolean {
|
||||
const store = readStore();
|
||||
if (!store[type][key]) return false;
|
||||
delete store[type][key];
|
||||
writeStore(store);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
81
src/routes/admin/onboarding-schemas/[schemaId].tsx
Normal file
81
src/routes/admin/onboarding-schemas/[schemaId].tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
|
||||
import type { RuntimeOnboardingConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function EditOnboardingPage() {
|
||||
const params = useParams();
|
||||
const [config, setConfig] = createSignal<RuntimeOnboardingConfig | null>(null);
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeOnboardingConfig>('onboarding', params.schemaId);
|
||||
setConfig(existing?.payload || null);
|
||||
});
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload) return;
|
||||
saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.');
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Edit Onboarding Flow</h1>
|
||||
<p class="page-subtitle">Update this runtime onboarding schema without changing source code.</p>
|
||||
{!config() ? (
|
||||
<section class="card"><p class="notice">Onboarding schema not found in local runtime storage.</p></section>
|
||||
) : (
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Onboarding Builder</h2>
|
||||
<div class="field">
|
||||
<label>Schema ID</label>
|
||||
<input value={config()!.schemaId} onInput={(e) => setConfig({ ...config()!, schemaId: e.currentTarget.value })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Role Key</label>
|
||||
<input value={config()!.roleKey} onInput={(e) => setConfig({ ...config()!, roleKey: e.currentTarget.value.toUpperCase() })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Version</label>
|
||||
<input type="number" value={config()!.version} onInput={(e) => setConfig({ ...config()!, version: Number(e.currentTarget.value || 1) })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Steps (one title per line, quick mode)</label>
|
||||
<textarea
|
||||
rows={8}
|
||||
value={config()!.steps.map((s) => s.title).join('\n')}
|
||||
onInput={(e) =>
|
||||
setConfig({
|
||||
...config()!,
|
||||
steps: e.currentTarget.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((title, index) => ({
|
||||
id: `step_${index + 1}`,
|
||||
title,
|
||||
fields: [{ id: `field_${index + 1}`, label: 'Sample Field', type: 'text', required: true }],
|
||||
})),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Runtime Config Preview</h2>
|
||||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
48
src/routes/admin/onboarding-schemas/index.tsx
Normal file
48
src/routes/admin/onboarding-schemas/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
|
||||
import type { RuntimeOnboardingConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function ManageOnboardingPage() {
|
||||
const [items, setItems] = createSignal<Array<RuntimeListItem<RuntimeOnboardingConfig>>>([]);
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeOnboardingConfig>('onboarding'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('onboarding', key);
|
||||
load();
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Manage Onboarding Flows</h1>
|
||||
<p class="page-subtitle">Edit or remove runtime onboarding schemas saved from the builder.</p>
|
||||
<section class="card">
|
||||
<div class="list-header">
|
||||
<h2>Runtime Onboarding Schemas</h2>
|
||||
<A class="btn primary" href="/admin/onboarding-schemas/new">Create Onboarding Flow</A>
|
||||
</div>
|
||||
{items().length === 0 ? (
|
||||
<p class="notice">No runtime onboarding schemas found yet.</p>
|
||||
) : (
|
||||
<div class="list-grid">
|
||||
{items().map((item) => (
|
||||
<article class="list-item">
|
||||
<h3>{item.key}</h3>
|
||||
<p>Status: <strong>{item.status}</strong></p>
|
||||
<p>Updated: {new Date(item.updatedAt).toLocaleString()}</p>
|
||||
<div class="actions">
|
||||
<A class="btn" href={`/admin/onboarding-schemas/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
89
src/routes/admin/role-ui-configs/[roleKey].tsx
Normal file
89
src/routes/admin/role-ui-configs/[roleKey].tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
|
||||
import type { RuntimeDashboardConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function EditDashboardPage() {
|
||||
const params = useParams();
|
||||
const [config, setConfig] = createSignal<RuntimeDashboardConfig | null>(null);
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeDashboardConfig>('dashboard', params.roleKey);
|
||||
setConfig(existing?.payload || null);
|
||||
});
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload) return;
|
||||
saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.');
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Edit Dashboard</h1>
|
||||
<p class="page-subtitle">Update dashboard runtime config without changing source code.</p>
|
||||
{!config() ? (
|
||||
<section class="card"><p class="notice">Dashboard config not found in local runtime storage.</p></section>
|
||||
) : (
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Dashboard Builder</h2>
|
||||
<div class="field">
|
||||
<label>Role Key</label>
|
||||
<input value={config()!.roleKey} onInput={(e) => setConfig({ ...config()!, roleKey: e.currentTarget.value.toUpperCase() })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sidebar Items (label|route per line)</label>
|
||||
<textarea
|
||||
rows={8}
|
||||
value={config()!.sidebar.map((x) => `${x.label}|${x.route}`).join('\n')}
|
||||
onInput={(e) =>
|
||||
setConfig({
|
||||
...config()!,
|
||||
sidebar: e.currentTarget.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [label, route] = line.split('|').map((part) => part.trim());
|
||||
return { key: label.toLowerCase().replace(/\s+/g, '-'), label, route };
|
||||
}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Widgets (title per line)</label>
|
||||
<textarea
|
||||
rows={6}
|
||||
value={config()!.widgets.map((x) => x.title).join('\n')}
|
||||
onInput={(e) =>
|
||||
setConfig({
|
||||
...config()!,
|
||||
widgets: e.currentTarget.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((title) => ({ key: title.toLowerCase().replace(/\s+/g, '-'), title, enabled: true })),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Runtime Config Preview</h2>
|
||||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
48
src/routes/admin/role-ui-configs/index.tsx
Normal file
48
src/routes/admin/role-ui-configs/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
|
||||
import type { RuntimeDashboardConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function ManageDashboardsPage() {
|
||||
const [items, setItems] = createSignal<Array<RuntimeListItem<RuntimeDashboardConfig>>>([]);
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeDashboardConfig>('dashboard'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('dashboard', key);
|
||||
load();
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Manage Dashboards</h1>
|
||||
<p class="page-subtitle">Edit or remove runtime dashboard configs saved from the builder.</p>
|
||||
<section class="card">
|
||||
<div class="list-header">
|
||||
<h2>Runtime Dashboards</h2>
|
||||
<A class="btn primary" href="/admin/role-ui-configs/new">Create Dashboard</A>
|
||||
</div>
|
||||
{items().length === 0 ? (
|
||||
<p class="notice">No runtime dashboard configs found yet.</p>
|
||||
) : (
|
||||
<div class="list-grid">
|
||||
{items().map((item) => (
|
||||
<article class="list-item">
|
||||
<h3>{item.key}</h3>
|
||||
<p>Status: <strong>{item.status}</strong></p>
|
||||
<p>Updated: {new Date(item.updatedAt).toLocaleString()}</p>
|
||||
<div class="actions">
|
||||
<A class="btn" href={`/admin/role-ui-configs/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
81
src/routes/admin/runtime-roles/[roleKey].tsx
Normal file
81
src/routes/admin/runtime-roles/[roleKey].tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
|
||||
import type { RuntimeRoleConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function EditRolePage() {
|
||||
const params = useParams();
|
||||
const [config, setConfig] = createSignal<RuntimeRoleConfig | null>(null);
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeRoleConfig>('role', params.roleKey);
|
||||
setConfig(existing?.payload || null);
|
||||
});
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload) return;
|
||||
saveRuntimeConfig('role', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.');
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Edit Role</h1>
|
||||
<p class="page-subtitle">Update this runtime role without changing source code.</p>
|
||||
{!config() ? (
|
||||
<section class="card"><p class="notice">Role not found in local runtime storage.</p></section>
|
||||
) : (
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Role Builder</h2>
|
||||
<div class="field">
|
||||
<label>Role Key</label>
|
||||
<input value={config()!.roleKey} onInput={(e) => setConfig({ ...config()!, roleKey: e.currentTarget.value.toUpperCase() })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Display Name</label>
|
||||
<input value={config()!.displayName} onInput={(e) => setConfig({ ...config()!, displayName: e.currentTarget.value })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Vertical</label>
|
||||
<select value={config()!.vertical} onInput={(e) => setConfig({ ...config()!, vertical: e.currentTarget.value as 'jobs' | 'marketplace' })}>
|
||||
<option value="marketplace">Marketplace</option>
|
||||
<option value="jobs">Jobs</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Onboarding Schema ID</label>
|
||||
<input value={config()!.onboardingSchemaId} onInput={(e) => setConfig({ ...config()!, onboardingSchemaId: e.currentTarget.value })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Enabled Modules (comma separated)</label>
|
||||
<input value={config()!.enabledModules.join(', ')} onInput={(e) => setConfig({ ...config()!, enabledModules: e.currentTarget.value.split(',').map((x) => x.trim()).filter(Boolean) })} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config()!.requiresOnboardingApproval}
|
||||
onInput={(e) => setConfig({ ...config()!, requiresOnboardingApproval: e.currentTarget.checked })}
|
||||
/>
|
||||
{' '}Requires onboarding approval
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Runtime Config Preview</h2>
|
||||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
48
src/routes/admin/runtime-roles/index.tsx
Normal file
48
src/routes/admin/runtime-roles/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage';
|
||||
import type { RuntimeRoleConfig } from '~/lib/runtime/types';
|
||||
|
||||
export default function ManageRolesPage() {
|
||||
const [items, setItems] = createSignal<Array<RuntimeListItem<RuntimeRoleConfig>>>([]);
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeRoleConfig>('role'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('role', key);
|
||||
load();
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Manage Roles</h1>
|
||||
<p class="page-subtitle">Edit or remove runtime role configs saved from the builder.</p>
|
||||
<section class="card">
|
||||
<div class="list-header">
|
||||
<h2>Runtime Roles</h2>
|
||||
<A class="btn primary" href="/admin/runtime-roles/new">Create Role</A>
|
||||
</div>
|
||||
{items().length === 0 ? (
|
||||
<p class="notice">No runtime role configs found yet.</p>
|
||||
) : (
|
||||
<div class="list-grid">
|
||||
{items().map((item) => (
|
||||
<article class="list-item">
|
||||
<h3>{item.key}</h3>
|
||||
<p>Status: <strong>{item.status}</strong></p>
|
||||
<p>Updated: {new Date(item.updatedAt).toLocaleString()}</p>
|
||||
<div class="actions">
|
||||
<A class="btn" href={`/admin/runtime-roles/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue