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 Plus from 'lucide-solid/icons/plus';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
@ -39,6 +39,7 @@ const emptyForm = (): BackendFormState => ({
export const Backends: Component = () => {
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 [confirmOpen, setConfirmOpen] = createSignal(false);
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.">
<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." />}
>
<Show
when={(backends()?.length ?? 0) > 0}
when={(currentBackends()?.length ?? 0) > 0}
fallback={
<EmptyState
title="No backends yet"
@ -216,7 +217,7 @@ export const Backends: Component = () => {
}
>
<DataGrid
rows={backends() ?? []}
rows={currentBackends() ?? []}
columns={[
{ id: 'id', header: 'ID', mono: true, cell: (backend) => <span>{backend.id}</span> },
{ id: 'name', header: 'Name', cell: (backend) => <span>{backend.name}</span> },
@ -248,7 +249,7 @@ export const Backends: Component = () => {
},
]}
getRowKey={(backend) => backend.id}
loading={backends.loading}
loading={backends.loading && (currentBackends()?.length ?? 0) === 0}
rowActions={(backend) => (
<div class="ui-row-actions">
<IconButton

View file

@ -41,6 +41,9 @@ export const Models: Component = () => {
const [overview, { refetch: refetchOverview }] = createResource(() => api.modelCache.getOverview());
const [backends] = createResource(() => api.backends.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 [confirmOpen, setConfirmOpen] = createSignal(false);
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 backendNameById = createMemo(() => {
const names = new Map<number, string>();
for (const backend of backends() ?? []) {
for (const backend of currentBackends() ?? []) {
names.set(backend.id, backend.name);
}
return names;
@ -58,7 +61,7 @@ export const Models: Component = () => {
const getBackendName = (backendId: number) => backendNameById().get(backendId) ?? `Backend ${backendId}`;
const modelCatalogRows = createMemo(() =>
(overview()?.models ?? []).map((entry) => ({
(currentOverview()?.models ?? []).map((entry) => ({
...entry,
backend_names: entry.backend_ids.map((backendId) => getBackendName(backendId)).join(', '),
}))
@ -151,9 +154,9 @@ export const Models: Component = () => {
<SummaryStrip
items={[
{ label: 'Catalog Models', value: overview()?.models.length ?? 0, hint: 'Unique models across active backends' },
{ label: 'Tracked Backends', value: overview()?.backends.length ?? 0, hint: 'Memory cache status by backend' },
{ label: 'Rewrite Rules', value: rules()?.length ?? 0, hint: 'Global source -> target mappings' },
{ label: 'Catalog Models', value: currentOverview()?.models.length ?? 0, hint: 'Unique models across active backends' },
{ label: 'Tracked Backends', value: currentOverview()?.backends.length ?? 0, hint: 'Memory cache status by backend' },
{ 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">
<Panel title="Backend Cache Status" description="Memory-backed backend cache state used by request routing and `/v1/models`.">
<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." />}
>
<DataGrid
rows={overview()?.backends ?? []}
rows={currentOverview()?.backends ?? []}
columns={[
{
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> },
]}
getRowKey={(item) => item.backend_id}
loading={overview.loading}
loading={overview.loading && (currentOverview()?.backends.length ?? 0) === 0}
/>
</Show>
</Panel>
<Panel title="Model Catalog" description="Unique models and the backend names currently advertising each one.">
<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." />}
>
<DataGrid
@ -216,7 +219,7 @@ export const Models: Component = () => {
},
]}
getRowKey={(item) => item.model_id}
loading={overview.loading}
loading={overview.loading && modelCatalogRows().length === 0}
/>
</Show>
</Panel>
@ -229,11 +232,11 @@ export const Models: Component = () => {
>
<div class="ui-stack ui-stack--tight">
<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." />}
>
<DataGrid
rows={rules() ?? []}
rows={currentRules() ?? []}
columns={[
{ id: 'source_model', header: 'Source', cell: (rule) => <span>{rule.source_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> },
]}
getRowKey={(rule) => rule.id}
loading={rules.loading}
loading={rules.loading && (currentRules()?.length ?? 0) === 0}
rowActions={(rule) => (
<div class="ui-row-actions">
<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 [users, { refetch: refetchUsers }] = createResource(() => api.users.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 [selectedScriptId, setSelectedScriptId] = createSignal<number | 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 [testing, setTesting] = createSignal(false);
const userOptions = createMemo(() => (users() ?? []).map((user) => ({ value: String(user.id), label: user.name })));
const backendOptions = createMemo(() => (backends() ?? []).map((backend) => ({ value: String(backend.id), label: backend.name })));
const userOptions = createMemo(() => (currentUsers() ?? []).map((user) => ({ value: String(user.id), label: user.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 selectedScript = createMemo(() => (scripts() ?? []).find((script) => script.id === selectedScriptId()) ?? null);
const activeCount = createMemo(() => (currentScripts() ?? []).filter((script) => script.is_active).length);
const selectedScript = createMemo(() => (currentScripts() ?? []).find((script) => script.id === selectedScriptId()) ?? null);
const syncForm = (script?: UserScript | null) => {
if (!script) {
@ -144,8 +147,8 @@ export const Scripts: Component = () => {
};
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 backend = (backends() ?? []).find((item) => item.id === script.target_backend_id);
const user = (currentUsers() ?? []).find((item) => item.id === script.target_user_id);
const backend = (currentBackends() ?? []).find((item) => item.id === script.target_backend_id);
if (script.script_type === 'per-user-backend') {
return {
@ -275,8 +278,8 @@ export const Scripts: Component = () => {
setTestResult(null);
try {
const result = await api.scripts.test(current.id, {
user: users()?.[0] || undefined,
backend: backends()?.[0] || undefined,
user: currentUsers()?.[0] || undefined,
backend: currentBackends()?.[0] || undefined,
request: {
method: 'POST',
path: '/v1/chat/completions',
@ -329,11 +332,11 @@ export const Scripts: Component = () => {
bodyClass="ui-stack ui-stack--tight"
>
<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." />}
>
<Show
when={(scripts()?.length ?? 0) > 0}
when={(currentScripts()?.length ?? 0) > 0}
fallback={
<EmptyState
title="No scripts yet"
@ -345,7 +348,7 @@ export const Scripts: Component = () => {
}
>
<DataGrid
rows={scripts() ?? []}
rows={currentScripts() ?? []}
columns={[
{
id: 'name',
@ -377,7 +380,7 @@ export const Scripts: Component = () => {
},
]}
getRowKey={(script) => script.id}
loading={scripts.loading}
loading={scripts.loading && (currentScripts()?.length ?? 0) === 0}
onRowClick={(script) => syncForm(script)}
rowActions={(script) => (
<div class="ui-row-actions">

View file

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