feat(routes): implement memoization for resource states in Backends, Models, Scripts, and Users components

This commit is contained in:
Kyush 2026-05-12 16:17:32 +09:00
commit 472e289198
4 changed files with 49 additions and 39 deletions

View file

@ -1,4 +1,4 @@
import { For, createResource, createSignal, Show, type Component } from 'solid-js'; import { For, createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
import Pencil from 'lucide-solid/icons/pencil'; import Pencil from 'lucide-solid/icons/pencil';
import Plus from 'lucide-solid/icons/plus'; import Plus from 'lucide-solid/icons/plus';
import RefreshCw from 'lucide-solid/icons/refresh-cw'; import RefreshCw from 'lucide-solid/icons/refresh-cw';
@ -39,6 +39,7 @@ const emptyForm = (): BackendFormState => ({
export const Backends: Component = () => { export const Backends: Component = () => {
const [backends, { refetch }] = createResource(() => api.backends.getAll()); const [backends, { refetch }] = createResource(() => api.backends.getAll());
const currentBackends = createMemo(() => backends.state === 'ready' || backends.state === 'refreshing' ? backends.latest : undefined);
const [dialogOpen, setDialogOpen] = createSignal(false); const [dialogOpen, setDialogOpen] = createSignal(false);
const [confirmOpen, setConfirmOpen] = createSignal(false); const [confirmOpen, setConfirmOpen] = createSignal(false);
const [editingBackend, setEditingBackend] = createSignal<Backend | null>(null); const [editingBackend, setEditingBackend] = createSignal<Backend | null>(null);
@ -202,11 +203,11 @@ export const Backends: Component = () => {
<Panel title="Backend catalog" description="Operational list with overflow-safe URL presentation and compact actions."> <Panel title="Backend catalog" description="Operational list with overflow-safe URL presentation and compact actions.">
<Show <Show
when={!backends.loading || (backends()?.length ?? 0) > 0} when={!backends.loading || (currentBackends()?.length ?? 0) > 0}
fallback={<EmptyState title="Loading backends" description="Reading upstream routing targets from the admin API." />} fallback={<EmptyState title="Loading backends" description="Reading upstream routing targets from the admin API." />}
> >
<Show <Show
when={(backends()?.length ?? 0) > 0} when={(currentBackends()?.length ?? 0) > 0}
fallback={ fallback={
<EmptyState <EmptyState
title="No backends yet" title="No backends yet"
@ -216,7 +217,7 @@ export const Backends: Component = () => {
} }
> >
<DataGrid <DataGrid
rows={backends() ?? []} rows={currentBackends() ?? []}
columns={[ columns={[
{ id: 'id', header: 'ID', mono: true, cell: (backend) => <span>{backend.id}</span> }, { id: 'id', header: 'ID', mono: true, cell: (backend) => <span>{backend.id}</span> },
{ id: 'name', header: 'Name', cell: (backend) => <span>{backend.name}</span> }, { id: 'name', header: 'Name', cell: (backend) => <span>{backend.name}</span> },
@ -248,7 +249,7 @@ export const Backends: Component = () => {
}, },
]} ]}
getRowKey={(backend) => backend.id} getRowKey={(backend) => backend.id}
loading={backends.loading} loading={backends.loading && (currentBackends()?.length ?? 0) === 0}
rowActions={(backend) => ( rowActions={(backend) => (
<div class="ui-row-actions"> <div class="ui-row-actions">
<IconButton <IconButton

View file

@ -41,6 +41,9 @@ export const Models: Component = () => {
const [overview, { refetch: refetchOverview }] = createResource(() => api.modelCache.getOverview()); const [overview, { refetch: refetchOverview }] = createResource(() => api.modelCache.getOverview());
const [backends] = createResource(() => api.backends.getAll()); const [backends] = createResource(() => api.backends.getAll());
const [rules, { refetch: refetchRules }] = createResource(() => api.modelRewrites.getAll()); const [rules, { refetch: refetchRules }] = createResource(() => api.modelRewrites.getAll());
const currentOverview = createMemo(() => overview.state === 'ready' || overview.state === 'refreshing' ? overview.latest : undefined);
const currentBackends = createMemo(() => backends.state === 'ready' || backends.state === 'refreshing' ? backends.latest : undefined);
const currentRules = createMemo(() => rules.state === 'ready' || rules.state === 'refreshing' ? rules.latest : undefined);
const [dialogOpen, setDialogOpen] = createSignal(false); const [dialogOpen, setDialogOpen] = createSignal(false);
const [confirmOpen, setConfirmOpen] = createSignal(false); const [confirmOpen, setConfirmOpen] = createSignal(false);
const [editingRule, setEditingRule] = createSignal<ModelRewriteRule | null>(null); const [editingRule, setEditingRule] = createSignal<ModelRewriteRule | null>(null);
@ -50,7 +53,7 @@ export const Models: Component = () => {
const [notice, setNotice] = createSignal<{ tone: 'success' | 'danger'; message: string } | null>(null); const [notice, setNotice] = createSignal<{ tone: 'success' | 'danger'; message: string } | null>(null);
const backendNameById = createMemo(() => { const backendNameById = createMemo(() => {
const names = new Map<number, string>(); const names = new Map<number, string>();
for (const backend of backends() ?? []) { for (const backend of currentBackends() ?? []) {
names.set(backend.id, backend.name); names.set(backend.id, backend.name);
} }
return names; return names;
@ -58,7 +61,7 @@ export const Models: Component = () => {
const getBackendName = (backendId: number) => backendNameById().get(backendId) ?? `Backend ${backendId}`; const getBackendName = (backendId: number) => backendNameById().get(backendId) ?? `Backend ${backendId}`;
const modelCatalogRows = createMemo(() => const modelCatalogRows = createMemo(() =>
(overview()?.models ?? []).map((entry) => ({ (currentOverview()?.models ?? []).map((entry) => ({
...entry, ...entry,
backend_names: entry.backend_ids.map((backendId) => getBackendName(backendId)).join(', '), backend_names: entry.backend_ids.map((backendId) => getBackendName(backendId)).join(', '),
})) }))
@ -151,9 +154,9 @@ export const Models: Component = () => {
<SummaryStrip <SummaryStrip
items={[ items={[
{ label: 'Catalog Models', value: overview()?.models.length ?? 0, hint: 'Unique models across active backends' }, { label: 'Catalog Models', value: currentOverview()?.models.length ?? 0, hint: 'Unique models across active backends' },
{ label: 'Tracked Backends', value: overview()?.backends.length ?? 0, hint: 'Memory cache status by backend' }, { label: 'Tracked Backends', value: currentOverview()?.backends.length ?? 0, hint: 'Memory cache status by backend' },
{ label: 'Rewrite Rules', value: rules()?.length ?? 0, hint: 'Global source -> target mappings' }, { label: 'Rewrite Rules', value: currentRules()?.length ?? 0, hint: 'Global source -> target mappings' },
]} ]}
/> />
@ -164,11 +167,11 @@ export const Models: Component = () => {
<div class="ui-section-grid"> <div class="ui-section-grid">
<Panel title="Backend Cache Status" description="Memory-backed backend cache state used by request routing and `/v1/models`."> <Panel title="Backend Cache Status" description="Memory-backed backend cache state used by request routing and `/v1/models`.">
<Show <Show
when={(overview()?.backends.length ?? 0) > 0} when={(currentOverview()?.backends.length ?? 0) > 0 || overview.loading}
fallback={<EmptyState title="No backend cache yet" description="Backend model states appear here after the server has seen active backends." />} fallback={<EmptyState title="No backend cache yet" description="Backend model states appear here after the server has seen active backends." />}
> >
<DataGrid <DataGrid
rows={overview()?.backends ?? []} rows={currentOverview()?.backends ?? []}
columns={[ columns={[
{ {
id: 'backend_id', id: 'backend_id',
@ -182,14 +185,14 @@ export const Models: Component = () => {
{ id: 'last_error', header: 'Last Error', cell: (item) => <span title={item.last_error ?? '-'}>{item.last_error ?? '-'}</span> }, { id: 'last_error', header: 'Last Error', cell: (item) => <span title={item.last_error ?? '-'}>{item.last_error ?? '-'}</span> },
]} ]}
getRowKey={(item) => item.backend_id} getRowKey={(item) => item.backend_id}
loading={overview.loading} loading={overview.loading && (currentOverview()?.backends.length ?? 0) === 0}
/> />
</Show> </Show>
</Panel> </Panel>
<Panel title="Model Catalog" description="Unique models and the backend names currently advertising each one."> <Panel title="Model Catalog" description="Unique models and the backend names currently advertising each one.">
<Show <Show
when={modelCatalogRows().length > 0} when={modelCatalogRows().length > 0 || overview.loading}
fallback={<EmptyState title="No cached models yet" description="Model catalog entries appear here after backend model snapshots are available." />} fallback={<EmptyState title="No cached models yet" description="Model catalog entries appear here after backend model snapshots are available." />}
> >
<DataGrid <DataGrid
@ -216,7 +219,7 @@ export const Models: Component = () => {
}, },
]} ]}
getRowKey={(item) => item.model_id} getRowKey={(item) => item.model_id}
loading={overview.loading} loading={overview.loading && modelCatalogRows().length === 0}
/> />
</Show> </Show>
</Panel> </Panel>
@ -229,11 +232,11 @@ export const Models: Component = () => {
> >
<div class="ui-stack ui-stack--tight"> <div class="ui-stack ui-stack--tight">
<Show <Show
when={(rules()?.length ?? 0) > 0} when={(currentRules()?.length ?? 0) > 0 || rules.loading}
fallback={<EmptyState title="No rewrite rules" description="Requests currently route using the original model name." />} fallback={<EmptyState title="No rewrite rules" description="Requests currently route using the original model name." />}
> >
<DataGrid <DataGrid
rows={rules() ?? []} rows={currentRules() ?? []}
columns={[ columns={[
{ id: 'source_model', header: 'Source', cell: (rule) => <span>{rule.source_model}</span> }, { id: 'source_model', header: 'Source', cell: (rule) => <span>{rule.source_model}</span> },
{ id: 'target_model', header: 'Target', cell: (rule) => <span>{rule.target_model}</span> }, { id: 'target_model', header: 'Target', cell: (rule) => <span>{rule.target_model}</span> },
@ -242,7 +245,7 @@ export const Models: Component = () => {
{ id: 'note', header: 'Note', cell: (rule) => <span title={rule.note ?? '-'}>{rule.note ?? '-'}</span> }, { id: 'note', header: 'Note', cell: (rule) => <span title={rule.note ?? '-'}>{rule.note ?? '-'}</span> },
]} ]}
getRowKey={(rule) => rule.id} getRowKey={(rule) => rule.id}
loading={rules.loading} loading={rules.loading && (currentRules()?.length ?? 0) === 0}
rowActions={(rule) => ( rowActions={(rule) => (
<div class="ui-row-actions"> <div class="ui-row-actions">
<IconButton icon={<Pencil />} label="Edit" onClick={() => openEditDialog(rule)} /> <IconButton icon={<Pencil />} label="Edit" onClick={() => openEditDialog(rule)} />

View file

@ -107,6 +107,9 @@ export const Scripts: Component = () => {
const [scripts, { refetch: refetchScripts }] = createResource(() => api.scripts.getAll()); const [scripts, { refetch: refetchScripts }] = createResource(() => api.scripts.getAll());
const [users, { refetch: refetchUsers }] = createResource(() => api.users.getAll()); const [users, { refetch: refetchUsers }] = createResource(() => api.users.getAll());
const [backends, { refetch: refetchBackends }] = createResource(() => api.backends.getAll()); const [backends, { refetch: refetchBackends }] = createResource(() => api.backends.getAll());
const currentScripts = createMemo(() => scripts.state === 'ready' || scripts.state === 'refreshing' ? scripts.latest : undefined);
const currentUsers = createMemo(() => users.state === 'ready' || users.state === 'refreshing' ? users.latest : undefined);
const currentBackends = createMemo(() => backends.state === 'ready' || backends.state === 'refreshing' ? backends.latest : undefined);
const [form, setForm] = createSignal<ScriptFormState>(emptyForm()); const [form, setForm] = createSignal<ScriptFormState>(emptyForm());
const [selectedScriptId, setSelectedScriptId] = createSignal<number | null>(null); const [selectedScriptId, setSelectedScriptId] = createSignal<number | null>(null);
const [pendingDeleteScript, setPendingDeleteScript] = createSignal<UserScript | null>(null); const [pendingDeleteScript, setPendingDeleteScript] = createSignal<UserScript | null>(null);
@ -116,11 +119,11 @@ export const Scripts: Component = () => {
const [testResult, setTestResult] = createSignal<{ success: boolean; error?: string; executionTime?: number } | null>(null); const [testResult, setTestResult] = createSignal<{ success: boolean; error?: string; executionTime?: number } | null>(null);
const [testing, setTesting] = createSignal(false); const [testing, setTesting] = createSignal(false);
const userOptions = createMemo(() => (users() ?? []).map((user) => ({ value: String(user.id), label: user.name }))); const userOptions = createMemo(() => (currentUsers() ?? []).map((user) => ({ value: String(user.id), label: user.name })));
const backendOptions = createMemo(() => (backends() ?? []).map((backend) => ({ value: String(backend.id), label: backend.name }))); const backendOptions = createMemo(() => (currentBackends() ?? []).map((backend) => ({ value: String(backend.id), label: backend.name })));
const activeCount = createMemo(() => (scripts() ?? []).filter((script) => script.is_active).length); const activeCount = createMemo(() => (currentScripts() ?? []).filter((script) => script.is_active).length);
const selectedScript = createMemo(() => (scripts() ?? []).find((script) => script.id === selectedScriptId()) ?? null); const selectedScript = createMemo(() => (currentScripts() ?? []).find((script) => script.id === selectedScriptId()) ?? null);
const syncForm = (script?: UserScript | null) => { const syncForm = (script?: UserScript | null) => {
if (!script) { if (!script) {
@ -144,8 +147,8 @@ export const Scripts: Component = () => {
}; };
const getTargetLabel = (script: Pick<UserScript, 'script_type' | 'target_user_id' | 'target_backend_id'>) => { const getTargetLabel = (script: Pick<UserScript, 'script_type' | 'target_user_id' | 'target_backend_id'>) => {
const user = (users() ?? []).find((item) => item.id === script.target_user_id); const user = (currentUsers() ?? []).find((item) => item.id === script.target_user_id);
const backend = (backends() ?? []).find((item) => item.id === script.target_backend_id); const backend = (currentBackends() ?? []).find((item) => item.id === script.target_backend_id);
if (script.script_type === 'per-user-backend') { if (script.script_type === 'per-user-backend') {
return { return {
@ -275,8 +278,8 @@ export const Scripts: Component = () => {
setTestResult(null); setTestResult(null);
try { try {
const result = await api.scripts.test(current.id, { const result = await api.scripts.test(current.id, {
user: users()?.[0] || undefined, user: currentUsers()?.[0] || undefined,
backend: backends()?.[0] || undefined, backend: currentBackends()?.[0] || undefined,
request: { request: {
method: 'POST', method: 'POST',
path: '/v1/chat/completions', path: '/v1/chat/completions',
@ -329,11 +332,11 @@ export const Scripts: Component = () => {
bodyClass="ui-stack ui-stack--tight" bodyClass="ui-stack ui-stack--tight"
> >
<Show <Show
when={!scripts.loading || (scripts()?.length ?? 0) > 0} when={!scripts.loading || (currentScripts()?.length ?? 0) > 0}
fallback={<EmptyState title="Loading scripts" description="Reading middleware definitions and target mappings." />} fallback={<EmptyState title="Loading scripts" description="Reading middleware definitions and target mappings." />}
> >
<Show <Show
when={(scripts()?.length ?? 0) > 0} when={(currentScripts()?.length ?? 0) > 0}
fallback={ fallback={
<EmptyState <EmptyState
title="No scripts yet" title="No scripts yet"
@ -345,7 +348,7 @@ export const Scripts: Component = () => {
} }
> >
<DataGrid <DataGrid
rows={scripts() ?? []} rows={currentScripts() ?? []}
columns={[ columns={[
{ {
id: 'name', id: 'name',
@ -377,7 +380,7 @@ export const Scripts: Component = () => {
}, },
]} ]}
getRowKey={(script) => script.id} getRowKey={(script) => script.id}
loading={scripts.loading} loading={scripts.loading && (currentScripts()?.length ?? 0) === 0}
onRowClick={(script) => syncForm(script)} onRowClick={(script) => syncForm(script)}
rowActions={(script) => ( rowActions={(script) => (
<div class="ui-row-actions"> <div class="ui-row-actions">

View file

@ -54,6 +54,9 @@ export const Users: Component = () => {
const [users, { refetch: refetchUsers }] = createResource(() => api.users.getAll()); const [users, { refetch: refetchUsers }] = createResource(() => api.users.getAll());
const [backends] = createResource(() => api.backends.getAll()); const [backends] = createResource(() => api.backends.getAll());
const [permissions, { refetch: refetchPermissions }] = createResource(() => api.permissions.getAll()); const [permissions, { refetch: refetchPermissions }] = createResource(() => api.permissions.getAll());
const currentUsers = createMemo(() => users.state === 'ready' || users.state === 'refreshing' ? users.latest : undefined);
const currentBackends = createMemo(() => backends.state === 'ready' || backends.state === 'refreshing' ? backends.latest : undefined);
const currentPermissions = createMemo(() => permissions.state === 'ready' || permissions.state === 'refreshing' ? permissions.latest : undefined);
const [query, setQuery] = createSignal(''); const [query, setQuery] = createSignal('');
const [dialogOpen, setDialogOpen] = createSignal(false); const [dialogOpen, setDialogOpen] = createSignal(false);
const [userDeleteConfirmOpen, setUserDeleteConfirmOpen] = createSignal(false); const [userDeleteConfirmOpen, setUserDeleteConfirmOpen] = createSignal(false);
@ -70,7 +73,7 @@ export const Users: Component = () => {
const filteredUsers = createMemo(() => { const filteredUsers = createMemo(() => {
const value = query().trim().toLowerCase(); const value = query().trim().toLowerCase();
const list = users() ?? []; const list = currentUsers() ?? [];
if (!value) return list; if (!value) return list;
return list.filter((user) => { return list.filter((user) => {
const haystack = [user.name, user.email ?? '', user.api_key].join(' ').toLowerCase(); const haystack = [user.name, user.email ?? '', user.api_key].join(' ').toLowerCase();
@ -78,29 +81,29 @@ export const Users: Component = () => {
}); });
}); });
const activeCount = createMemo(() => (users() ?? []).filter((user) => user.is_active).length); const activeCount = createMemo(() => (currentUsers() ?? []).filter((user) => user.is_active).length);
const selectedUser = createMemo(() => (users() ?? []).find((user) => user.id === selectedUserId()) ?? null); const selectedUser = createMemo(() => (currentUsers() ?? []).find((user) => user.id === selectedUserId()) ?? null);
const permissionsForSelectedUser = createMemo(() => { const permissionsForSelectedUser = createMemo(() => {
const currentUserId = selectedUserId(); const currentUserId = selectedUserId();
if (!currentUserId) return []; if (!currentUserId) return [];
return (permissions() ?? []).filter((permission) => permission.user_id === currentUserId); return (currentPermissions() ?? []).filter((permission) => permission.user_id === currentUserId);
}); });
const assignedBackendIds = createMemo(() => new Set(permissionsForSelectedUser().map((permission) => permission.backend_id))); const assignedBackendIds = createMemo(() => new Set(permissionsForSelectedUser().map((permission) => permission.backend_id)));
const availableBackendOptions = createMemo(() => const availableBackendOptions = createMemo(() =>
(backends() ?? []) (currentBackends() ?? [])
.filter((backend) => !assignedBackendIds().has(backend.id)) .filter((backend) => !assignedBackendIds().has(backend.id))
.map((backend) => ({ value: String(backend.id), label: backend.name })) .map((backend) => ({ value: String(backend.id), label: backend.name }))
); );
const backendNameById = createMemo(() => { const backendNameById = createMemo(() => {
const names = new Map<number, string>(); const names = new Map<number, string>();
for (const backend of backends() ?? []) { for (const backend of currentBackends() ?? []) {
names.set(backend.id, backend.name); names.set(backend.id, backend.name);
} }
return names; return names;
}); });
createEffect(() => { createEffect(() => {
const list = users() ?? []; const list = currentUsers() ?? [];
const currentSelectedUserId = selectedUserId(); const currentSelectedUserId = selectedUserId();
if (list.length === 0) { if (list.length === 0) {
@ -369,7 +372,7 @@ export const Users: Component = () => {
}, },
]} ]}
getRowKey={(user) => user.id} getRowKey={(user) => user.id}
loading={users.loading} loading={users.loading && filteredUsers().length === 0}
emptyMessage="No users match the current search." emptyMessage="No users match the current search."
onRowClick={(user) => setSelectedUserId(user.id)} onRowClick={(user) => setSelectedUserId(user.id)}
rowActions={(user) => ( rowActions={(user) => (
@ -455,7 +458,7 @@ export const Users: Component = () => {
}, },
]} ]}
getRowKey={(permission) => `${permission.user_id}-${permission.backend_id}`} getRowKey={(permission) => `${permission.user_id}-${permission.backend_id}`}
loading={permissions.loading || backends.loading} loading={(permissions.loading || backends.loading) && permissionsForSelectedUser().length === 0}
rowActions={(permission) => ( rowActions={(permission) => (
<IconButton <IconButton
variant="danger" variant="danger"