feat: align admin onboarding and role-ui-configs with migration schema
This commit is contained in:
parent
75b54368f0
commit
56803bd7b2
10 changed files with 378 additions and 184 deletions
|
|
@ -1,14 +1,8 @@
|
|||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding';
|
||||
export type RuntimeRecordStatus = 'draft' | 'published';
|
||||
|
||||
const KEY = 'nxtgauge_admin_runtime_builder_v1';
|
||||
|
||||
type RuntimeStore = {
|
||||
role: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
dashboard: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
onboarding: Record<string, RuntimeStoredRecord<unknown>>;
|
||||
};
|
||||
|
||||
export type RuntimeStoredRecord<T> = {
|
||||
status: RuntimeRecordStatus;
|
||||
updatedAt: string;
|
||||
|
|
@ -19,61 +13,130 @@ export type RuntimeListItem<T> = RuntimeStoredRecord<T> & {
|
|||
key: string;
|
||||
};
|
||||
|
||||
function emptyStore(): RuntimeStore {
|
||||
return { role: {}, dashboard: {}, onboarding: {} };
|
||||
}
|
||||
// Types corresponding to Rust backend models
|
||||
type RoleRecord = {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
audience: string;
|
||||
};
|
||||
|
||||
function readStore(): RuntimeStore {
|
||||
if (typeof window === 'undefined') return emptyStore();
|
||||
const raw = window.localStorage.getItem(KEY);
|
||||
if (!raw) return emptyStore();
|
||||
export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) {
|
||||
const normalizedKey = key.trim().toUpperCase();
|
||||
if (!normalizedKey) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<RuntimeStore>;
|
||||
return {
|
||||
role: parsed.role || {},
|
||||
dashboard: parsed.dashboard || {},
|
||||
onboarding: parsed.onboarding || {},
|
||||
// 1. Ensure the Role exists first. We lookup by key.
|
||||
// If it doesn't exist, we create it generically.
|
||||
let roleRes = await fetch(`${API_URL}/api/admin/roles/${normalizedKey}`);
|
||||
let role: RoleRecord;
|
||||
|
||||
if (!roleRes.ok && roleRes.status === 404) {
|
||||
// Create it
|
||||
const createRes = await fetch(`${API_URL}/api/admin/roles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: normalizedKey,
|
||||
name: normalizedKey, // Using key as name for now
|
||||
audience: 'EXTERNAL',
|
||||
})
|
||||
});
|
||||
if (!createRes.ok) throw new Error("Failed to create role");
|
||||
role = await createRes.json();
|
||||
} else {
|
||||
role = await roleRes.json();
|
||||
}
|
||||
|
||||
// 2. Attach the specific config payload to the role
|
||||
const endpointMap: Record<RuntimeRecordType, string> = {
|
||||
role: '/api/runtime-config', // Storing the "role config wrapper" as runtime_config
|
||||
dashboard: '/api/admin/dashboard-config',
|
||||
onboarding: '/api/admin/onboarding-config',
|
||||
};
|
||||
} catch {
|
||||
return emptyStore();
|
||||
|
||||
let bodyPayload: any = {
|
||||
role_id: role.id,
|
||||
};
|
||||
|
||||
if (type === 'dashboard') {
|
||||
bodyPayload.audience = 'EXTERNAL';
|
||||
bodyPayload.config_json = payload;
|
||||
} else if (type === 'onboarding') {
|
||||
bodyPayload.schema_json = payload;
|
||||
} else if (type === 'role') {
|
||||
bodyPayload.config_json = payload;
|
||||
}
|
||||
|
||||
const saveRes = await fetch(`${API_URL}${endpointMap[type]}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bodyPayload)
|
||||
});
|
||||
|
||||
if (!saveRes.ok) {
|
||||
throw new Error(`Failed to save ${type} config`);
|
||||
}
|
||||
|
||||
console.log(`Saved ${type} mapping for role ${normalizedKey} successfully to Rust backend`);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error saving runtime config [${type}]:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store: RuntimeStore) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(KEY, JSON.stringify(store));
|
||||
export async function listRuntimeConfigs<T>(type: RuntimeRecordType): Promise<Array<RuntimeListItem<T>>> {
|
||||
const endpointMap: Record<RuntimeRecordType, string> = {
|
||||
role: '/api/admin/roles',
|
||||
dashboard: '/api/admin/dashboard-config',
|
||||
onboarding: '/api/admin/onboarding-config',
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}${endpointMap[type]}`);
|
||||
if (!res.ok) throw new Error(`Failed to list ${type} configs`);
|
||||
const data = await res.json();
|
||||
|
||||
// Map Rust backend models to UI expected format
|
||||
return data.map((item: any) => ({
|
||||
key: item.role_key || item.key,
|
||||
status: item.is_active ? 'published' : 'draft',
|
||||
updatedAt: item.updated_at || item.created_at,
|
||||
payload: item.config_json || item.schema_json || item,
|
||||
// include raw id for routing convenience
|
||||
id: item.id,
|
||||
role_id: item.role_id
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`Error listing runtime config [${type}]:`, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) {
|
||||
const normalizedKey = key.trim();
|
||||
if (!normalizedKey) return;
|
||||
|
||||
const store = readStore();
|
||||
store[type][normalizedKey] = {
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
payload,
|
||||
};
|
||||
writeStore(store);
|
||||
export async function getRuntimeConfig<T>(type: RuntimeRecordType, roleId: string): Promise<RuntimeStoredRecord<T> | null> {
|
||||
const endpointMap: Record<RuntimeRecordType, string> = {
|
||||
role: `/api/admin/roles/${roleId}`, // Assume roleId is key if testing
|
||||
dashboard: `/api/admin/dashboard-config/${roleId}?audience=EXTERNAL`,
|
||||
onboarding: `/api/admin/onboarding-config/${roleId}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}${endpointMap[type]}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
|
||||
return {
|
||||
status: data.is_active ? 'published' : 'draft',
|
||||
updatedAt: data.updated_at || data.created_at,
|
||||
payload: data.config_json || data.schema_json || data,
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
export async function deleteRuntimeConfig(type: RuntimeRecordType, key: string): Promise<boolean> {
|
||||
// Mock delete since hard deletes usually aren't ideal for configs (we just soft-disable them on new save)
|
||||
console.log(`Delete requested for ${type} config with key ${key}. In Rust, we just save a new one with is_active=true overriding the old one.`);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createEffect, createResource, createSignal, Show } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage';
|
||||
import type { RuntimeOnboardingConfig, RuntimeOnboardingStep } from '~/lib/runtime/types';
|
||||
|
|
@ -10,16 +10,24 @@ function stringifySteps(steps: RuntimeOnboardingStep[]): string {
|
|||
|
||||
export default function EditOnboardingPage() {
|
||||
const params = useParams();
|
||||
const [data] = createResource(() => {
|
||||
if (!params.schemaId) return null;
|
||||
return getRuntimeConfig<RuntimeOnboardingConfig>('onboarding', params.schemaId);
|
||||
});
|
||||
|
||||
const [config, setConfig] = createSignal<RuntimeOnboardingConfig | null>(null);
|
||||
const [stepsJson, setStepsJson] = createSignal('[]');
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
const [stepsError, setStepsError] = createSignal('');
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeOnboardingConfig>('onboarding', params.schemaId);
|
||||
if (!existing?.payload) return;
|
||||
setConfig(existing.payload);
|
||||
setStepsJson(stringifySteps(existing.payload.steps));
|
||||
// Sync resource to local signals
|
||||
createEffect(() => {
|
||||
const item = data();
|
||||
if (item?.payload) {
|
||||
setConfig(JSON.parse(JSON.stringify(item.payload)));
|
||||
setStepsJson(stringifySteps(item.payload.steps));
|
||||
}
|
||||
});
|
||||
|
||||
const syncSteps = (raw: string) => {
|
||||
|
|
@ -40,7 +48,7 @@ export default function EditOnboardingPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const persist = async (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload) return;
|
||||
if (!payload.schemaId.trim()) {
|
||||
|
|
@ -56,17 +64,30 @@ export default function EditOnboardingPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.');
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Onboarding schema published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<h1 class="page-title">Edit Onboarding Flow</h1>
|
||||
<p class="page-subtitle">All onboarding fields, validations, upload rules, and select behaviors are runtime schema-driven.</p>
|
||||
{!config() ? (
|
||||
<section class="card"><p class="notice">Onboarding schema not found in local runtime storage.</p></section>
|
||||
) : (
|
||||
<Show when={data.loading}>
|
||||
<section class="card"><p class="notice">Loading onboarding schema from database...</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && !config()}>
|
||||
<section class="card"><p class="notice">Onboarding schema "{params.schemaId}" not found in database.</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && config()}>
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Onboarding Builder</h2>
|
||||
|
|
@ -88,8 +109,12 @@ export default function EditOnboardingPage() {
|
|||
{stepsError() && <p class="error-note">{stepsError()}</p>}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
@ -98,7 +123,7 @@ export default function EditOnboardingPage() {
|
|||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createResource, Show } 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 [items, { refetch }] = createResource(() => listRuntimeConfigs<RuntimeOnboardingConfig>('onboarding'));
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeOnboardingConfig>('onboarding'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('onboarding', key);
|
||||
load();
|
||||
const onDelete = async (key: string) => {
|
||||
await deleteRuntimeConfig('onboarding', key);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -25,23 +21,27 @@ export default function ManageOnboardingPage() {
|
|||
<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>
|
||||
)}
|
||||
<Show when={items.loading}>
|
||||
<p class="notice">Loading onboarding schemas from database...</p>
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && items()?.length === 0}>
|
||||
<p class="notice">No runtime onboarding schemas found yet.</p>
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||
<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).toLocaleDateString()}</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>
|
||||
</Show>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ export default function CreateOnboardingSchemaPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
const persist = async (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload.schemaId.trim()) {
|
||||
setStatusMessage('Schema ID is required before saving.');
|
||||
|
|
@ -69,8 +71,17 @@ export default function CreateOnboardingSchemaPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.');
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Onboarding schema published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -98,8 +109,12 @@ export default function CreateOnboardingSchemaPage() {
|
|||
{stepsError() && <p class="error-note">{stepsError()}</p>}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,61 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createResource, createSignal, createEffect, onMount, Show } 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 [data] = createResource(() => {
|
||||
if (!params.roleKey) return null;
|
||||
return getRuntimeConfig<RuntimeDashboardConfig>('dashboard', params.roleKey);
|
||||
});
|
||||
|
||||
const [config, setConfig] = createSignal<RuntimeDashboardConfig | null>(null);
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeDashboardConfig>('dashboard', params.roleKey);
|
||||
setConfig(existing?.payload || null);
|
||||
// We'll sync the resource to the signal once it loads
|
||||
// However, in Solid it's better to just use the resource or a derived signal
|
||||
});
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
// Effect to sync config once data is loaded
|
||||
createEffect(() => {
|
||||
const item = data();
|
||||
if (item?.payload) {
|
||||
setConfig(JSON.parse(JSON.stringify(item.payload)));
|
||||
}
|
||||
});
|
||||
|
||||
const persist = async (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.');
|
||||
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Dashboard config published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
) : (
|
||||
<Show when={data.loading}>
|
||||
<section class="card"><p class="notice">Loading dashboard configuration from database...</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && !config()}>
|
||||
<section class="card"><p class="notice">Dashboard config "{params.roleKey}" not found in database.</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && config()}>
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Dashboard Builder</h2>
|
||||
|
|
@ -73,8 +101,12 @@ export default function EditDashboardPage() {
|
|||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
@ -83,7 +115,7 @@ export default function EditDashboardPage() {
|
|||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createResource, Show } 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 [items, { refetch }] = createResource(() => listRuntimeConfigs<RuntimeDashboardConfig>('dashboard'));
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeDashboardConfig>('dashboard'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('dashboard', key);
|
||||
load();
|
||||
const onDelete = async (key: string) => {
|
||||
await deleteRuntimeConfig('dashboard', key);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -25,23 +21,27 @@ export default function ManageDashboardsPage() {
|
|||
<h2>Runtime Dashboards</h2>
|
||||
<A class="btn primary" href="/admin/role-ui-configs/new">Create Dashboard</A>
|
||||
</div>
|
||||
{items().length === 0 ? (
|
||||
<Show when={items.loading}>
|
||||
<p class="notice">Loading dashboards from database...</p>
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && 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>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||
<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).toLocaleDateString()}</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>
|
||||
</Show>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,14 +19,26 @@ export default function CreateDashboardPage() {
|
|||
});
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
const persist = async (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload.roleKey.trim()) {
|
||||
setStatusMessage('Role key is required before saving.');
|
||||
return;
|
||||
}
|
||||
saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.');
|
||||
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Dashboard config published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -78,8 +90,12 @@ export default function CreateDashboardPage() {
|
|||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,56 @@
|
|||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createEffect, createResource, createSignal, Show } 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 [data] = createResource(() => {
|
||||
if (!params.roleKey) return null;
|
||||
return getRuntimeConfig<RuntimeRoleConfig>('role', params.roleKey);
|
||||
});
|
||||
|
||||
const [config, setConfig] = createSignal<RuntimeRoleConfig | null>(null);
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const existing = getRuntimeConfig<RuntimeRoleConfig>('role', params.roleKey);
|
||||
setConfig(existing?.payload || null);
|
||||
// Sync resource to local signals
|
||||
createEffect(() => {
|
||||
const item = data();
|
||||
if (item?.payload) {
|
||||
setConfig(JSON.parse(JSON.stringify(item.payload)));
|
||||
}
|
||||
});
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const persist = async (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.');
|
||||
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('role', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Role config published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
) : (
|
||||
<Show when={data.loading}>
|
||||
<section class="card"><p class="notice">Loading role configuration from database...</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && !config()}>
|
||||
<section class="card"><p class="notice">Role "{params.roleKey}" not found in database.</p></section>
|
||||
</Show>
|
||||
<Show when={!data.loading && config()}>
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Role Builder</h2>
|
||||
|
|
@ -65,8 +88,12 @@ export default function EditRolePage() {
|
|||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
@ -75,7 +102,7 @@ export default function EditRolePage() {
|
|||
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import { createResource, Show } 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 [items, { refetch }] = createResource(() => listRuntimeConfigs<RuntimeRoleConfig>('role'));
|
||||
|
||||
const load = () => setItems(listRuntimeConfigs<RuntimeRoleConfig>('role'));
|
||||
|
||||
onMount(load);
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
deleteRuntimeConfig('role', key);
|
||||
load();
|
||||
const onDelete = async (key: string) => {
|
||||
await deleteRuntimeConfig('role', key);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -25,23 +21,27 @@ export default function ManageRolesPage() {
|
|||
<h2>Runtime Roles</h2>
|
||||
<A class="btn primary" href="/admin/runtime-roles/new">Create Role</A>
|
||||
</div>
|
||||
{items().length === 0 ? (
|
||||
<Show when={items.loading}>
|
||||
<p class="notice">Loading roles from database...</p>
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && 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>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||
<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).toLocaleDateString()}</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>
|
||||
</Show>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,14 +14,26 @@ export default function CreateRolePage() {
|
|||
});
|
||||
const [statusMessage, setStatusMessage] = createSignal('');
|
||||
|
||||
const persist = (status: 'draft' | 'published') => {
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
const persist = async (status: 'draft' | 'published') => {
|
||||
const payload = config();
|
||||
if (!payload.roleKey.trim()) {
|
||||
setStatusMessage('Role key is required before saving.');
|
||||
return;
|
||||
}
|
||||
saveRuntimeConfig('role', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.');
|
||||
|
||||
setIsSaving(true);
|
||||
setStatusMessage('Saving to backend...');
|
||||
|
||||
try {
|
||||
await saveRuntimeConfig('role', payload.roleKey, payload, status);
|
||||
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Role config published successfully.');
|
||||
} catch (err) {
|
||||
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -65,8 +77,12 @@ export default function CreateRolePage() {
|
|||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
|
||||
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
|
||||
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Saving...' : 'Save Draft'}
|
||||
</button>
|
||||
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
||||
{isSaving() ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue