@svadmin/ui 0.40.6 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AICommandBar.svelte +46 -23
- package/dist/components/AboutSettings.svelte +4 -3
- package/dist/components/AboutSettings.svelte.d.ts +2 -17
- package/dist/components/AdminApp.svelte +92 -20
- package/dist/components/AdminApp.svelte.d.ts +17 -2
- package/dist/components/AuditLogDrawer.svelte +45 -16
- package/dist/components/AuditLogViewer.svelte +32 -14
- package/dist/components/AutoTable.svelte +11 -5
- package/dist/components/CancelTaskButton.svelte +12 -4
- package/dist/components/ChatDialog.svelte +456 -122
- package/dist/components/ChatDialog.svelte.d.ts +9 -1
- package/dist/components/CommandPalette.svelte +2 -2
- package/dist/components/CopilotPanel.svelte +67 -26
- package/dist/components/DevTools.svelte +249 -22
- package/dist/components/InferencerPanel.svelte +25 -5
- package/dist/components/InsightCard.svelte +47 -17
- package/dist/components/Layout.svelte +45 -20
- package/dist/components/LoginPage.svelte +4 -4
- package/dist/components/ProfilePage.svelte +74 -27
- package/dist/components/RetryTaskButton.svelte +12 -4
- package/dist/components/RolesSettings.svelte +50 -21
- package/dist/components/SettingsPage.svelte +4 -4
- package/dist/components/Sidebar.svelte +26 -16
- package/dist/components/SmartSuggest.svelte +71 -32
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +2 -2
|
@@ -2,7 +2,11 @@ import type { ChatMessage, ChatAction } from '@svadmin/core';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
/** Render the launcher inside a layout toolbar instead of as a floating action button. */
|
|
4
4
|
docked?: boolean;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Panel suffix for chat history persistence. Both default and explicit
|
|
7
|
+
* suffixes are scoped by backend API + typed tenant identity.
|
|
8
|
+
* Set to '' to disable persistence.
|
|
9
|
+
*/
|
|
6
10
|
persistKey?: string;
|
|
7
11
|
/** Custom persistence callback — called when messages change */
|
|
8
12
|
onPersist?: (messages: ChatMessage[]) => void;
|
|
@@ -10,6 +14,10 @@ interface Props {
|
|
|
10
14
|
onRestore?: () => ChatMessage[];
|
|
11
15
|
/** Callback when user clicks an action button in an assistant message */
|
|
12
16
|
onAction?: (action: ChatAction, message: ChatMessage) => void;
|
|
17
|
+
/** Stable target for tree-scoped ask-ai events. */
|
|
18
|
+
scope?: string;
|
|
19
|
+
/** Owning Layout scope used to arbitrate global keyboard shortcuts. */
|
|
20
|
+
ownerScope?: string;
|
|
13
21
|
}
|
|
14
22
|
declare const ChatDialog: import("svelte").Component<Props, {}, "">;
|
|
15
23
|
type ChatDialog = ReturnType<typeof ChatDialog>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext, getResources,
|
|
2
|
+
import { captureAdminContext, getResources, toggleTheme } from '@svadmin/core';
|
|
3
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
4
|
import { Command } from './ui/command/index.js';
|
|
5
5
|
import * as Dialog from './ui/dialog/index.js';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
let searchValue = $state('');
|
|
19
19
|
|
|
20
20
|
const resources = $derived(getResources());
|
|
21
|
-
const hasAI = $derived(!!(
|
|
21
|
+
const hasAI = $derived(!!(adminContext.agentProvider || adminContext.chatProvider));
|
|
22
22
|
|
|
23
23
|
function close() {
|
|
24
24
|
open = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { captureAdminContext, useParsed, type ChatProvider } from '@svadmin/core';
|
|
3
3
|
import { Sparkles, X, Check, RefreshCcw } from '@lucide/svelte';
|
|
4
4
|
import { slide } from 'svelte/transition';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
@@ -8,60 +8,101 @@
|
|
|
8
8
|
|
|
9
9
|
let { open = $bindable(false) } = $props<{ open?: boolean }>();
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
11
|
+
const adminContext = captureAdminContext();
|
|
12
|
+
const provider = $derived(adminContext.chatProvider);
|
|
13
|
+
const parsed = useParsed();
|
|
14
|
+
const ctx = $derived({
|
|
15
|
+
currentResource: parsed.resource,
|
|
16
|
+
selectedRecordId: parsed.id,
|
|
17
|
+
currentView: parsed.action,
|
|
18
|
+
});
|
|
13
19
|
|
|
14
20
|
let isPredicting = $state(false);
|
|
15
21
|
let insightText = $state('');
|
|
16
22
|
let abortController: AbortController | null = null;
|
|
17
|
-
let
|
|
23
|
+
let requestEpoch = 0;
|
|
18
24
|
|
|
19
|
-
// Automatically request insights when the context changes
|
|
20
25
|
$effect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const scopedProvider = provider;
|
|
27
|
+
const scopedContext = {
|
|
28
|
+
currentResource: ctx.currentResource,
|
|
29
|
+
selectedRecordId: ctx.selectedRecordId,
|
|
30
|
+
currentView: ctx.currentView,
|
|
31
|
+
};
|
|
32
|
+
const shouldFetch = open;
|
|
33
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
34
|
+
|
|
35
|
+
clearCopilotScope();
|
|
36
|
+
if (shouldFetch && scopedProvider) {
|
|
37
|
+
void requestInsights(scopedProvider, scopedContext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return cancelCopilotRequest;
|
|
29
41
|
});
|
|
30
42
|
|
|
31
43
|
async function getInsights() {
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const scopedProvider = provider;
|
|
45
|
+
if (!scopedProvider) return;
|
|
46
|
+
await requestInsights(scopedProvider, {
|
|
47
|
+
currentResource: ctx.currentResource,
|
|
48
|
+
selectedRecordId: ctx.selectedRecordId,
|
|
49
|
+
currentView: ctx.currentView,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cancelCopilotRequest() {
|
|
54
|
+
requestEpoch += 1;
|
|
55
|
+
abortController?.abort();
|
|
56
|
+
abortController = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearCopilotScope() {
|
|
60
|
+
cancelCopilotRequest();
|
|
61
|
+
isPredicting = false;
|
|
62
|
+
insightText = '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function requestInsights(
|
|
66
|
+
scopedProvider: ChatProvider,
|
|
67
|
+
scopedContext: typeof ctx,
|
|
68
|
+
) {
|
|
69
|
+
cancelCopilotRequest();
|
|
70
|
+
const epoch = requestEpoch;
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
abortController = controller;
|
|
34
73
|
isPredicting = true;
|
|
35
74
|
insightText = '';
|
|
36
|
-
|
|
37
|
-
if (abortController) abortController.abort();
|
|
38
|
-
abortController = new AbortController();
|
|
39
75
|
|
|
40
76
|
const prompt = `Analyze the current context and provide 3 quick insights or context-aware suggestions for the admin user.
|
|
41
|
-
Resource: ${
|
|
42
|
-
View: ${
|
|
43
|
-
Record ID: ${
|
|
77
|
+
Resource: ${scopedContext.currentResource}
|
|
78
|
+
View: ${scopedContext.currentView}
|
|
79
|
+
Record ID: ${scopedContext.selectedRecordId || 'None'}
|
|
44
80
|
Keep it concise, professional, and use bullet points.`;
|
|
45
81
|
|
|
46
82
|
try {
|
|
47
|
-
const result =
|
|
83
|
+
const result = scopedProvider.sendMessage(
|
|
48
84
|
[{ id: 'insights', role: 'system', content: prompt, timestamp: Date.now() }],
|
|
49
|
-
{ signal:
|
|
85
|
+
{ signal: controller.signal }
|
|
50
86
|
);
|
|
51
87
|
|
|
52
88
|
if (Symbol.asyncIterator in result) {
|
|
53
89
|
for await (const chunk of result as AsyncIterable<string>) {
|
|
90
|
+
if (epoch !== requestEpoch) return;
|
|
54
91
|
insightText += chunk;
|
|
55
92
|
}
|
|
56
93
|
} else {
|
|
57
|
-
|
|
94
|
+
const response = await result;
|
|
95
|
+
if (epoch === requestEpoch) insightText = response;
|
|
58
96
|
}
|
|
59
97
|
} catch (err: unknown) {
|
|
60
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
98
|
+
if (epoch === requestEpoch && err instanceof Error && err.name !== 'AbortError') {
|
|
61
99
|
insightText = 'Failed to generate insights for this context.';
|
|
62
100
|
}
|
|
63
101
|
} finally {
|
|
64
|
-
|
|
102
|
+
if (epoch === requestEpoch) {
|
|
103
|
+
isPredicting = false;
|
|
104
|
+
if (abortController === controller) abortController = null;
|
|
105
|
+
}
|
|
65
106
|
}
|
|
66
107
|
}
|
|
67
108
|
</script>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
3
|
import { fly } from 'svelte/transition';
|
|
4
|
-
import {
|
|
4
|
+
import { useQueryClient } from '@tanstack/svelte-query';
|
|
5
|
+
import {
|
|
6
|
+
captureAdminContext,
|
|
7
|
+
getColorTheme,
|
|
8
|
+
getResources,
|
|
9
|
+
getTheme,
|
|
10
|
+
} from '@svadmin/core';
|
|
5
11
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
12
|
|
|
7
13
|
import TooltipButton from './TooltipButton.svelte';
|
|
@@ -12,43 +18,189 @@
|
|
|
12
18
|
import { X, Bug, ChevronDown, ChevronUp, Wand2 } from '@lucide/svelte';
|
|
13
19
|
import InferencerPanel from './InferencerPanel.svelte';
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
type CacheDiagnostics = {
|
|
22
|
+
queries: { total: number; fetching: number; stale: number; errors: number };
|
|
23
|
+
mutations: { total: number; pending: number; paused: number; errors: number };
|
|
24
|
+
};
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
type ProviderDiagnostic = {
|
|
27
|
+
name: string;
|
|
28
|
+
configured: boolean;
|
|
29
|
+
capabilities: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const EMPTY_CACHE_DIAGNOSTICS: CacheDiagnostics = {
|
|
33
|
+
queries: { total: 0, fetching: 0, stale: 0, errors: 0 },
|
|
34
|
+
mutations: { total: 0, pending: 0, paused: 0, errors: 0 },
|
|
35
|
+
};
|
|
18
36
|
|
|
37
|
+
const isDev = import.meta.env.DEV;
|
|
38
|
+
const i18n = useTranslation();
|
|
39
|
+
const queryClient = useQueryClient();
|
|
19
40
|
const adminContext = captureAdminContext();
|
|
41
|
+
|
|
42
|
+
let { docked = false }: { docked?: boolean } = $props();
|
|
20
43
|
let visible = $state(false);
|
|
21
44
|
let collapsed = $state(false);
|
|
45
|
+
let cacheDiagnostics = $state.raw<CacheDiagnostics>(EMPTY_CACHE_DIAGNOSTICS);
|
|
22
46
|
|
|
23
47
|
function toggle() {
|
|
24
48
|
visible = !visible;
|
|
25
49
|
}
|
|
26
50
|
|
|
27
|
-
function handleKeydown(
|
|
28
|
-
if (
|
|
29
|
-
|
|
51
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
52
|
+
if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'd') {
|
|
53
|
+
event.preventDefault();
|
|
30
54
|
toggle();
|
|
31
55
|
}
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
58
|
+
function refreshCacheDiagnostics() {
|
|
59
|
+
const queries = queryClient.getQueryCache().getAll();
|
|
60
|
+
const mutations = queryClient.getMutationCache().getAll();
|
|
61
|
+
cacheDiagnostics = {
|
|
62
|
+
queries: {
|
|
63
|
+
total: queries.length,
|
|
64
|
+
fetching: queries.filter((query) => query.state.fetchStatus === 'fetching').length,
|
|
65
|
+
stale: queries.filter((query) => query.isStale()).length,
|
|
66
|
+
errors: queries.filter((query) => query.state.status === 'error').length,
|
|
67
|
+
},
|
|
68
|
+
mutations: {
|
|
69
|
+
total: mutations.length,
|
|
70
|
+
pending: mutations.filter((mutation) => mutation.state.status === 'pending').length,
|
|
71
|
+
paused: mutations.filter((mutation) => mutation.state.isPaused).length,
|
|
72
|
+
errors: mutations.filter((mutation) => mutation.state.status === 'error').length,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMount(() => {
|
|
78
|
+
if (!isDev) return;
|
|
79
|
+
refreshCacheDiagnostics();
|
|
80
|
+
const unsubscribeQueries = queryClient.getQueryCache().subscribe(refreshCacheDiagnostics);
|
|
81
|
+
const unsubscribeMutations = queryClient.getMutationCache().subscribe(refreshCacheDiagnostics);
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribeQueries();
|
|
84
|
+
unsubscribeMutations();
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const resources = $derived.by(() => {
|
|
89
|
+
try {
|
|
90
|
+
return getResources();
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
35
95
|
const path = $derived(adminContext.currentPath());
|
|
36
96
|
const theme = $derived(getTheme());
|
|
37
97
|
const colorTheme = $derived(getColorTheme());
|
|
38
98
|
const locale = $derived(i18n.locale);
|
|
39
99
|
|
|
100
|
+
const dataProviders = $derived.by(() => {
|
|
101
|
+
try {
|
|
102
|
+
return adminContext.getDataProviderNames().map((name) => {
|
|
103
|
+
const provider = adminContext.getDataProvider(name);
|
|
104
|
+
const capabilities = [
|
|
105
|
+
provider.getMany || provider.createMany || provider.updateMany || provider.deleteMany ? 'bulk' : undefined,
|
|
106
|
+
provider.custom ? 'custom' : undefined,
|
|
107
|
+
].filter((capability): capability is string => Boolean(capability));
|
|
108
|
+
const resourceCount = resources.filter((resource) =>
|
|
109
|
+
(resource.provider?.dataProviderName ?? resource.meta?.dataProviderName ?? 'default') === name,
|
|
110
|
+
).length;
|
|
111
|
+
return {
|
|
112
|
+
name,
|
|
113
|
+
resourceCount,
|
|
114
|
+
capabilities: capabilities.length > 0 ? capabilities.join(', ') : 'CRUD',
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const frameworkProviders = $derived.by((): ProviderDiagnostic[] => {
|
|
123
|
+
const access = adminContext.accessControlProvider;
|
|
124
|
+
const audit = adminContext.auditLogProvider;
|
|
125
|
+
const notification = adminContext.notificationProvider;
|
|
126
|
+
const chat = adminContext.chatProvider;
|
|
127
|
+
const agent = adminContext.agentProvider;
|
|
128
|
+
const live = adminContext.liveProvider;
|
|
129
|
+
const task = adminContext.taskProvider;
|
|
130
|
+
const auth = adminContext.authProvider;
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
name: 'Auth',
|
|
135
|
+
configured: Boolean(auth),
|
|
136
|
+
capabilities: auth
|
|
137
|
+
? [auth.getPermissions ? 'permissions' : undefined, auth.getRoles ? 'roles' : undefined]
|
|
138
|
+
.filter((capability): capability is string => Boolean(capability))
|
|
139
|
+
.join(', ') || 'identity'
|
|
140
|
+
: 'not configured',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'Access control',
|
|
144
|
+
configured: Boolean(access),
|
|
145
|
+
capabilities: access?.options?.buttons?.enableAccessControl ? 'button checks enabled' : access ? 'policy checks' : 'not configured',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Live',
|
|
149
|
+
configured: Boolean(live),
|
|
150
|
+
capabilities: live
|
|
151
|
+
? ['subscribe', live.publish ? 'publish' : undefined, live.unsubscribe ? 'unsubscribe' : undefined]
|
|
152
|
+
.filter((capability): capability is string => Boolean(capability))
|
|
153
|
+
.join(', ')
|
|
154
|
+
: 'not configured',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'Audit',
|
|
158
|
+
configured: Boolean(audit),
|
|
159
|
+
capabilities: audit ? 'create, get, update' : 'not configured',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Notification',
|
|
163
|
+
configured: Boolean(notification),
|
|
164
|
+
capabilities: notification ? 'open, close' : 'built-in toast fallback',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Chat',
|
|
168
|
+
configured: Boolean(chat),
|
|
169
|
+
capabilities: chat ? 'message stream' : 'not configured',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'Agent',
|
|
173
|
+
configured: Boolean(agent),
|
|
174
|
+
capabilities: agent ? `${agent.tools?.length ?? 0} tools (names hidden)` : 'not configured',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'Task',
|
|
178
|
+
configured: Boolean(task),
|
|
179
|
+
capabilities: task
|
|
180
|
+
? [task.list ? 'list' : undefined, task.subscribe ? 'subscribe' : undefined, task.retry ? 'retry' : undefined, task.cancel ? 'cancel' : undefined]
|
|
181
|
+
.filter((capability): capability is string => Boolean(capability))
|
|
182
|
+
.join(', ') || 'submit, get'
|
|
183
|
+
: 'not configured',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'Tenant',
|
|
187
|
+
configured: Boolean(adminContext.tenant),
|
|
188
|
+
capabilities: adminContext.tenant ? 'tree scoped (identifier hidden)' : 'not configured',
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
});
|
|
40
192
|
</script>
|
|
41
193
|
|
|
42
|
-
<svelte:window onkeydown={handleKeydown} />
|
|
194
|
+
<svelte:window onkeydown={isDev ? handleKeydown : undefined} />
|
|
43
195
|
|
|
44
|
-
{#if
|
|
196
|
+
{#if isDev}
|
|
45
197
|
{#if visible}
|
|
46
198
|
<div
|
|
47
|
-
class="fixed bottom-0 right-4 z-[9999] w-[
|
|
48
|
-
class:w-auto={collapsed}
|
|
199
|
+
class="fixed bottom-0 right-4 z-[9999] w-[560px] max-w-[95vw] rounded-t-xl border border-b-0 bg-card shadow-2xl text-[0.8125rem] overflow-hidden"
|
|
200
|
+
class:w-auto={collapsed}
|
|
201
|
+
class:min-w-[200px]={collapsed}
|
|
49
202
|
transition:fly={{ y: 400, duration: 300 }}
|
|
50
203
|
>
|
|
51
|
-
<!-- Header -->
|
|
52
204
|
<div class="flex items-center justify-between px-3 py-2 bg-muted border-b">
|
|
53
205
|
<div class="flex items-center gap-1.5 font-semibold text-xs uppercase tracking-wider text-foreground">
|
|
54
206
|
<Bug class="h-4 w-4" />
|
|
@@ -70,16 +222,18 @@
|
|
|
70
222
|
|
|
71
223
|
{#if !collapsed}
|
|
72
224
|
<Tabs.Root value="state" class="w-full">
|
|
73
|
-
<Tabs.List class="grid w-full grid-cols-
|
|
225
|
+
<Tabs.List class="grid w-full grid-cols-4 h-8 rounded-none border-b">
|
|
74
226
|
<Tabs.Trigger value="state" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">State</Tabs.Trigger>
|
|
227
|
+
<Tabs.Trigger value="providers" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Providers</Tabs.Trigger>
|
|
228
|
+
<Tabs.Trigger value="cache" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Cache</Tabs.Trigger>
|
|
75
229
|
<Tabs.Trigger value="inferencer" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">
|
|
76
230
|
<Wand2 class="h-3 w-3 mr-1" /> Inferencer
|
|
77
231
|
</Tabs.Trigger>
|
|
78
232
|
</Tabs.List>
|
|
233
|
+
|
|
79
234
|
<Tabs.Content value="state" class="p-0">
|
|
80
|
-
<ScrollArea class="max-h-[
|
|
235
|
+
<ScrollArea class="max-h-[420px]">
|
|
81
236
|
<div class="p-2 space-y-1">
|
|
82
|
-
<!-- Router -->
|
|
83
237
|
<div class="py-1">
|
|
84
238
|
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Router</h4>
|
|
85
239
|
<div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
|
|
@@ -90,7 +244,6 @@
|
|
|
90
244
|
|
|
91
245
|
<Separator />
|
|
92
246
|
|
|
93
|
-
<!-- Theme -->
|
|
94
247
|
<div class="py-1">
|
|
95
248
|
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Theme</h4>
|
|
96
249
|
<div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
|
|
@@ -105,7 +258,6 @@
|
|
|
105
258
|
|
|
106
259
|
<Separator />
|
|
107
260
|
|
|
108
|
-
<!-- i18n -->
|
|
109
261
|
<div class="py-1">
|
|
110
262
|
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">i18n</h4>
|
|
111
263
|
<div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
|
|
@@ -116,19 +268,94 @@
|
|
|
116
268
|
|
|
117
269
|
<Separator />
|
|
118
270
|
|
|
119
|
-
<!-- Resources -->
|
|
120
271
|
<div class="py-1">
|
|
121
272
|
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Resources ({resources.length})</h4>
|
|
122
|
-
{#each resources as
|
|
273
|
+
{#each resources as resource (resource.identifier ?? resource.name)}
|
|
123
274
|
<div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
|
|
124
|
-
<span class="text-xs text-foreground">{
|
|
125
|
-
<Badge variant="outline" class="font-mono text-[0.6875rem]">{
|
|
275
|
+
<span class="text-xs text-foreground">{resource.name}</span>
|
|
276
|
+
<Badge variant="outline" class="font-mono text-[0.6875rem]">{resource.fields.length} fields</Badge>
|
|
126
277
|
</div>
|
|
127
278
|
{/each}
|
|
128
279
|
</div>
|
|
129
280
|
</div>
|
|
130
281
|
</ScrollArea>
|
|
131
282
|
</Tabs.Content>
|
|
283
|
+
|
|
284
|
+
<Tabs.Content value="providers" class="p-0">
|
|
285
|
+
<ScrollArea class="max-h-[420px]">
|
|
286
|
+
<div class="p-3 space-y-3">
|
|
287
|
+
<div class="rounded-md border bg-muted/20 p-2 text-[0.6875rem] text-muted-foreground">
|
|
288
|
+
Sensitive values are hidden: endpoints, credentials, record data, messages, and live/audit payloads.
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<section class="space-y-1.5">
|
|
292
|
+
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground">Data providers ({dataProviders.length})</h4>
|
|
293
|
+
{#each dataProviders as provider (provider.name)}
|
|
294
|
+
<div class="rounded-md border px-2.5 py-2">
|
|
295
|
+
<div class="flex items-center justify-between gap-2">
|
|
296
|
+
<span class="font-medium text-foreground">{provider.name}</span>
|
|
297
|
+
<Badge variant="outline">{provider.resourceCount} resources</Badge>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="mt-1 flex items-center justify-between gap-2 text-[0.6875rem] text-muted-foreground">
|
|
300
|
+
<span>{provider.capabilities}</span>
|
|
301
|
+
<span>Endpoint hidden</span>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
{/each}
|
|
305
|
+
</section>
|
|
306
|
+
|
|
307
|
+
<Separator />
|
|
308
|
+
|
|
309
|
+
<section class="space-y-1">
|
|
310
|
+
<h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground">Framework providers</h4>
|
|
311
|
+
{#each frameworkProviders as provider (provider.name)}
|
|
312
|
+
<div class="flex items-center justify-between gap-3 rounded px-1 py-1 hover:bg-muted/50">
|
|
313
|
+
<div class="min-w-0">
|
|
314
|
+
<div class="text-xs text-foreground">{provider.name}</div>
|
|
315
|
+
<div class="truncate text-[0.6875rem] text-muted-foreground">{provider.capabilities}</div>
|
|
316
|
+
</div>
|
|
317
|
+
<Badge variant={provider.configured ? 'secondary' : 'outline'}>{provider.configured ? 'configured' : 'fallback'}</Badge>
|
|
318
|
+
</div>
|
|
319
|
+
{/each}
|
|
320
|
+
</section>
|
|
321
|
+
</div>
|
|
322
|
+
</ScrollArea>
|
|
323
|
+
</Tabs.Content>
|
|
324
|
+
|
|
325
|
+
<Tabs.Content value="cache" class="p-0">
|
|
326
|
+
<ScrollArea class="max-h-[420px]">
|
|
327
|
+
<div class="grid gap-3 p-3 sm:grid-cols-2">
|
|
328
|
+
<section class="rounded-md border p-3">
|
|
329
|
+
<div class="mb-2 flex items-center justify-between">
|
|
330
|
+
<h4 class="text-xs font-semibold text-foreground">Queries</h4>
|
|
331
|
+
<Badge variant="secondary" data-testid="devtools-query-total">{cacheDiagnostics.queries.total}</Badge>
|
|
332
|
+
</div>
|
|
333
|
+
<dl class="space-y-1 text-xs">
|
|
334
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Fetching</dt><dd>{cacheDiagnostics.queries.fetching}</dd></div>
|
|
335
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Stale</dt><dd>{cacheDiagnostics.queries.stale}</dd></div>
|
|
336
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Errors</dt><dd>{cacheDiagnostics.queries.errors}</dd></div>
|
|
337
|
+
</dl>
|
|
338
|
+
</section>
|
|
339
|
+
|
|
340
|
+
<section class="rounded-md border p-3">
|
|
341
|
+
<div class="mb-2 flex items-center justify-between">
|
|
342
|
+
<h4 class="text-xs font-semibold text-foreground">Mutations</h4>
|
|
343
|
+
<Badge variant="secondary" data-testid="devtools-mutation-total">{cacheDiagnostics.mutations.total}</Badge>
|
|
344
|
+
</div>
|
|
345
|
+
<dl class="space-y-1 text-xs">
|
|
346
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Pending</dt><dd>{cacheDiagnostics.mutations.pending}</dd></div>
|
|
347
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Paused</dt><dd>{cacheDiagnostics.mutations.paused}</dd></div>
|
|
348
|
+
<div class="flex justify-between"><dt class="text-muted-foreground">Errors</dt><dd>{cacheDiagnostics.mutations.errors}</dd></div>
|
|
349
|
+
</dl>
|
|
350
|
+
</section>
|
|
351
|
+
|
|
352
|
+
<p class="text-[0.6875rem] text-muted-foreground sm:col-span-2">
|
|
353
|
+
Query keys, variables, cached records, mutation payloads, and error bodies are intentionally hidden.
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
</ScrollArea>
|
|
357
|
+
</Tabs.Content>
|
|
358
|
+
|
|
132
359
|
<Tabs.Content value="inferencer" class="p-0">
|
|
133
360
|
<InferencerPanel />
|
|
134
361
|
</Tabs.Content>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext, inferResource } from '@svadmin/core';
|
|
4
4
|
import type { InferResult } from '@svadmin/core';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
import { Select } from './ui/select/index.js';
|
|
14
14
|
import { Wand2, Copy, Check, RefreshCw, Loader2, AlertCircle } from '@lucide/svelte';
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
const resources =
|
|
16
|
+
const adminContext = captureAdminContext();
|
|
17
|
+
const resources = $derived(adminContext.resources);
|
|
18
18
|
|
|
19
19
|
let selectedResource = $state('');
|
|
20
20
|
let inferResult = $state<InferResult | null>(null);
|
|
@@ -23,24 +23,42 @@
|
|
|
23
23
|
let copied = $state(false);
|
|
24
24
|
let customEndpoint = $state('');
|
|
25
25
|
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
26
|
+
let inferenceEpoch = 0;
|
|
26
27
|
|
|
27
28
|
$effect(() => {
|
|
28
29
|
return () => { if (copyTimer) clearTimeout(copyTimer); };
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
$effect(() => {
|
|
33
|
+
void adminContext.providerBundle;
|
|
34
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
35
|
+
void resources;
|
|
36
|
+
inferenceEpoch += 1;
|
|
37
|
+
inferResult = null;
|
|
38
|
+
loading = false;
|
|
39
|
+
error = null;
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
inferenceEpoch += 1;
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
31
46
|
async function runInference() {
|
|
32
47
|
const resourceName = customEndpoint.trim() || selectedResource;
|
|
33
48
|
if (!resourceName) return;
|
|
34
49
|
|
|
50
|
+
const epoch = ++inferenceEpoch;
|
|
35
51
|
loading = true;
|
|
36
52
|
error = null;
|
|
37
53
|
inferResult = null;
|
|
38
54
|
|
|
39
55
|
try {
|
|
56
|
+
const dataProvider = adminContext.getDataProviderForResource(resourceName);
|
|
40
57
|
const response = await dataProvider.getList({
|
|
41
58
|
resource: resourceName,
|
|
42
59
|
pagination: { current: 1, pageSize: 25 },
|
|
43
60
|
});
|
|
61
|
+
if (epoch !== inferenceEpoch) return;
|
|
44
62
|
|
|
45
63
|
if (!response.data || response.data.length === 0) {
|
|
46
64
|
error = `No data returned from "${resourceName}". The API must return at least one record to infer fields.`;
|
|
@@ -52,9 +70,11 @@
|
|
|
52
70
|
response.data as Record<string, unknown>[],
|
|
53
71
|
);
|
|
54
72
|
} catch (e: unknown) {
|
|
55
|
-
|
|
73
|
+
if (epoch === inferenceEpoch) {
|
|
74
|
+
error = e instanceof Error ? e.message : 'Failed to fetch data for inference.';
|
|
75
|
+
}
|
|
56
76
|
} finally {
|
|
57
|
-
loading = false;
|
|
77
|
+
if (epoch === inferenceEpoch) loading = false;
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|