@svadmin/ui 0.40.5 → 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/components/ui/input/input.svelte +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext,
|
|
2
|
+
import { captureAdminContext, 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';
|
|
@@ -12,24 +12,42 @@
|
|
|
12
12
|
const adminContext = captureAdminContext();
|
|
13
13
|
let searchValue = $state('');
|
|
14
14
|
|
|
15
|
-
const resources =
|
|
16
|
-
const provider = $derived(
|
|
15
|
+
const resources = $derived(adminContext.resources);
|
|
16
|
+
const provider = $derived(adminContext.chatProvider);
|
|
17
17
|
|
|
18
18
|
// AI State
|
|
19
19
|
let aiMode = $state(false);
|
|
20
20
|
let aiResponse = $state('');
|
|
21
21
|
let isPredicting = $state(false);
|
|
22
22
|
let abortController: AbortController | null = null;
|
|
23
|
+
let requestEpoch = 0;
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
void provider;
|
|
27
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
28
|
+
void open;
|
|
29
|
+
clearAIConversation();
|
|
30
|
+
|
|
31
|
+
return cancelAIRequest;
|
|
32
|
+
});
|
|
23
33
|
|
|
24
34
|
function close() {
|
|
25
35
|
open = false;
|
|
36
|
+
clearAIConversation();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cancelAIRequest() {
|
|
40
|
+
requestEpoch += 1;
|
|
41
|
+
abortController?.abort();
|
|
42
|
+
abortController = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearAIConversation() {
|
|
46
|
+
cancelAIRequest();
|
|
26
47
|
searchValue = '';
|
|
27
48
|
aiMode = false;
|
|
28
49
|
aiResponse = '';
|
|
29
|
-
|
|
30
|
-
abortController.abort();
|
|
31
|
-
abortController = null;
|
|
32
|
-
}
|
|
50
|
+
isPredicting = false;
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
function act(fn: () => void) {
|
|
@@ -38,45 +56,50 @@
|
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
async function askAI() {
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
const scopedProvider = provider;
|
|
60
|
+
const prompt = searchValue.trim();
|
|
61
|
+
if (!scopedProvider || !prompt) return;
|
|
62
|
+
|
|
63
|
+
cancelAIRequest();
|
|
64
|
+
const epoch = requestEpoch;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
abortController = controller;
|
|
43
67
|
aiMode = true;
|
|
44
68
|
isPredicting = true;
|
|
45
69
|
aiResponse = '';
|
|
46
|
-
|
|
47
|
-
if (abortController) abortController.abort();
|
|
48
|
-
abortController = new AbortController();
|
|
49
70
|
|
|
50
71
|
try {
|
|
51
|
-
const result =
|
|
52
|
-
[{ id: 'cmd', role: 'user', content:
|
|
53
|
-
{ signal:
|
|
72
|
+
const result = scopedProvider.sendMessage(
|
|
73
|
+
[{ id: 'cmd', role: 'user', content: prompt, timestamp: Date.now() }],
|
|
74
|
+
{ signal: controller.signal }
|
|
54
75
|
);
|
|
55
76
|
|
|
56
77
|
if (Symbol.asyncIterator in result) {
|
|
57
78
|
for await (const chunk of result as AsyncIterable<string>) {
|
|
79
|
+
if (epoch !== requestEpoch) return;
|
|
58
80
|
aiResponse += chunk;
|
|
59
81
|
}
|
|
60
82
|
} else {
|
|
61
|
-
|
|
83
|
+
const response = await result;
|
|
84
|
+
if (epoch === requestEpoch) aiResponse = response;
|
|
62
85
|
}
|
|
63
86
|
} catch (err: unknown) {
|
|
64
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
87
|
+
if (epoch === requestEpoch && err instanceof Error && err.name !== 'AbortError') {
|
|
65
88
|
aiResponse = 'Failed to get response from AI.';
|
|
66
89
|
}
|
|
67
90
|
} finally {
|
|
68
|
-
|
|
91
|
+
if (epoch === requestEpoch) {
|
|
92
|
+
isPredicting = false;
|
|
93
|
+
if (abortController === controller) abortController = null;
|
|
94
|
+
}
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
function handleKeyDown(e: KeyboardEvent) {
|
|
73
99
|
if (e.key === 'Enter' && searchValue.trim() && !aiMode) {
|
|
74
|
-
// If pressing enter on an active search, cmdk usually handles selection.
|
|
75
|
-
// But if there's no selection or we want to force AI, we can intercept.
|
|
76
|
-
// For simplicity, we trigger AI on Ctrl+Enter
|
|
77
100
|
if (e.ctrlKey || e.metaKey) {
|
|
78
101
|
e.preventDefault();
|
|
79
|
-
askAI();
|
|
102
|
+
void askAI();
|
|
80
103
|
}
|
|
81
104
|
}
|
|
82
105
|
}
|
|
@@ -170,7 +193,7 @@
|
|
|
170
193
|
<div class="p-2 border-t flex justify-end gap-2 bg-muted/10">
|
|
171
194
|
<button
|
|
172
195
|
class="text-xs px-3 py-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground"
|
|
173
|
-
onclick={
|
|
196
|
+
onclick={clearAIConversation}
|
|
174
197
|
>
|
|
175
198
|
Clear & Return
|
|
176
199
|
</button>
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext } from '@svadmin/core';
|
|
4
4
|
import { getResolvedTheme, getColorTheme } from '@svadmin/core';
|
|
5
5
|
import * as Card from './ui/card/index.js';
|
|
6
6
|
import { Badge } from './ui/badge/index.js';
|
|
7
7
|
import { Info, Database, Globe, Palette, Layers } from '@lucide/svelte';
|
|
8
8
|
|
|
9
9
|
const i18n = useTranslation();
|
|
10
|
+
const adminContext = captureAdminContext();
|
|
10
11
|
|
|
11
|
-
const resources =
|
|
12
|
-
const providerNames = getDataProviderNames();
|
|
12
|
+
const resources = $derived(adminContext.resources);
|
|
13
|
+
const providerNames = $derived(adminContext.getDataProviderNames());
|
|
13
14
|
|
|
14
15
|
const version = '__SVADMIN_VERSION__'; // replaced at build time or shown as-is
|
|
15
16
|
</script>
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
$$bindings?: Bindings;
|
|
4
|
-
} & Exports;
|
|
5
|
-
(internal: unknown, props: {
|
|
6
|
-
$$events?: Events;
|
|
7
|
-
$$slots?: Slots;
|
|
8
|
-
}): Exports & {
|
|
9
|
-
$set?: any;
|
|
10
|
-
$on?: any;
|
|
11
|
-
};
|
|
12
|
-
z_$$bindings?: Bindings;
|
|
13
|
-
}
|
|
14
|
-
declare const AboutSettings: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
-
[evt: string]: CustomEvent<any>;
|
|
16
|
-
}, {}, {}, string>;
|
|
17
|
-
type AboutSettings = InstanceType<typeof AboutSettings>;
|
|
1
|
+
declare const AboutSettings: import("svelte").Component<Record<string, never>, {}, "">;
|
|
2
|
+
type AboutSettings = ReturnType<typeof AboutSettings>;
|
|
18
3
|
export default AboutSettings;
|
|
@@ -19,7 +19,26 @@
|
|
|
19
19
|
// Auto-inject design tokens so consumers don't need to manually import
|
|
20
20
|
import '../app.css';
|
|
21
21
|
import { onDestroy, onMount, untrack, type Component, type Snippet } from 'svelte';
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AccessControlProvider,
|
|
24
|
+
AgentProvider,
|
|
25
|
+
AuditLogProvider,
|
|
26
|
+
AuthProvider,
|
|
27
|
+
ChatProvider,
|
|
28
|
+
DataProviderInput,
|
|
29
|
+
I18nProvider,
|
|
30
|
+
LiveProvider,
|
|
31
|
+
MenuItem,
|
|
32
|
+
NotificationProvider,
|
|
33
|
+
ProviderBundle,
|
|
34
|
+
ResourceDefinition,
|
|
35
|
+
RouterProvider,
|
|
36
|
+
TaskProvider,
|
|
37
|
+
TenantAdapter,
|
|
38
|
+
TenantContext,
|
|
39
|
+
ThemeConfig,
|
|
40
|
+
ThemeMode,
|
|
41
|
+
} from '@svadmin/core';
|
|
23
42
|
import {
|
|
24
43
|
captureAdminContext,
|
|
25
44
|
createHashRouterProvider,
|
|
@@ -55,12 +74,27 @@
|
|
|
55
74
|
import { Badge } from './ui/badge/index.js';
|
|
56
75
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
57
76
|
import { provideRouterState } from '../router-state.svelte.js';
|
|
77
|
+
import type { AdminProviderBundle } from '../types.js';
|
|
58
78
|
|
|
59
79
|
interface Props {
|
|
60
|
-
|
|
80
|
+
/** Single provider, or named providers such as `{ default, analytics }`. */
|
|
81
|
+
dataProvider?: DataProviderInput;
|
|
82
|
+
/** Core canonical provider bundle. Existing top-level props take precedence. */
|
|
83
|
+
providerBundle?: AdminProviderBundle;
|
|
84
|
+
/** Compatibility alias for providerBundle. */
|
|
85
|
+
providers?: AdminProviderBundle;
|
|
61
86
|
authProvider?: AuthProvider;
|
|
87
|
+
accessControlProvider?: AccessControlProvider;
|
|
88
|
+
liveProvider?: LiveProvider;
|
|
89
|
+
auditLogProvider?: AuditLogProvider;
|
|
90
|
+
notificationProvider?: NotificationProvider;
|
|
91
|
+
chatProvider?: ChatProvider;
|
|
92
|
+
agentProvider?: AgentProvider;
|
|
62
93
|
taskProvider?: TaskProvider;
|
|
63
94
|
routerProvider?: RouterProvider;
|
|
95
|
+
tenantAdapter?: TenantAdapter;
|
|
96
|
+
/** Request/tree-scoped tenant. Tenant identifiers are never rendered by DevTools. */
|
|
97
|
+
tenant?: TenantContext;
|
|
64
98
|
resources: ResourceDefinition[];
|
|
65
99
|
/** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
|
|
66
100
|
locale?: string;
|
|
@@ -102,9 +136,19 @@
|
|
|
102
136
|
|
|
103
137
|
let {
|
|
104
138
|
dataProvider,
|
|
139
|
+
providerBundle,
|
|
140
|
+
providers,
|
|
105
141
|
authProvider,
|
|
142
|
+
accessControlProvider,
|
|
143
|
+
liveProvider,
|
|
144
|
+
auditLogProvider,
|
|
145
|
+
notificationProvider,
|
|
146
|
+
chatProvider,
|
|
147
|
+
agentProvider,
|
|
106
148
|
taskProvider,
|
|
107
149
|
routerProvider,
|
|
150
|
+
tenantAdapter,
|
|
151
|
+
tenant,
|
|
108
152
|
resources,
|
|
109
153
|
locale = $bindable(),
|
|
110
154
|
i18nProvider,
|
|
@@ -134,10 +178,24 @@
|
|
|
134
178
|
Skeleton: Skeleton as unknown as ComponentRegistry['Skeleton'],
|
|
135
179
|
};
|
|
136
180
|
|
|
181
|
+
const resolvedProviderBundle = $derived(providerBundle ?? providers);
|
|
182
|
+
const resolvedDataProvider = $derived(dataProvider ?? resolvedProviderBundle?.dataProvider);
|
|
183
|
+
const resolvedAuthProvider = $derived(authProvider ?? resolvedProviderBundle?.authProvider ?? undefined);
|
|
184
|
+
const resolvedAccessControlProvider = $derived(accessControlProvider ?? resolvedProviderBundle?.accessControlProvider ?? undefined);
|
|
185
|
+
const resolvedLiveProvider = $derived(liveProvider ?? resolvedProviderBundle?.liveProvider);
|
|
186
|
+
const resolvedAuditLogProvider = $derived(auditLogProvider ?? resolvedProviderBundle?.auditLogProvider ?? undefined);
|
|
187
|
+
const resolvedNotificationProvider = $derived(notificationProvider ?? resolvedProviderBundle?.notificationProvider ?? undefined);
|
|
188
|
+
const resolvedChatProvider = $derived(chatProvider ?? resolvedProviderBundle?.chatProvider ?? undefined);
|
|
189
|
+
const resolvedAgentProvider = $derived(agentProvider ?? resolvedProviderBundle?.agentProvider ?? undefined);
|
|
190
|
+
const resolvedTaskProvider = $derived(taskProvider ?? resolvedProviderBundle?.taskProvider);
|
|
191
|
+
const resolvedRouterInput = $derived(routerProvider ?? resolvedProviderBundle?.routerProvider);
|
|
192
|
+
const resolvedTenantAdapter = $derived(tenantAdapter ?? resolvedProviderBundle?.tenantAdapter);
|
|
193
|
+
const resolvedI18nProvider = $derived(i18nProvider ?? resolvedProviderBundle?.i18nProvider);
|
|
194
|
+
|
|
137
195
|
// Resolve router provider (default to hash)
|
|
138
196
|
const mergedComponents = $derived.by((): ComponentRegistry => ({ ...defaultComponents, ...userComponents }));
|
|
139
|
-
const resolvedRouter = $derived(
|
|
140
|
-
const resolvedRouteMode = $derived(routeMode ?? (
|
|
197
|
+
const resolvedRouter = $derived(resolvedRouterInput ?? createHashRouterProvider());
|
|
198
|
+
const resolvedRouteMode = $derived(routeMode ?? (resolvedRouterInput ? 'auto' : 'hash'));
|
|
141
199
|
|
|
142
200
|
const routerState = provideRouterState(untrack(() => resolvedRouter));
|
|
143
201
|
const scopedRouterProvider: RouterProvider = {
|
|
@@ -166,14 +224,14 @@
|
|
|
166
224
|
|
|
167
225
|
const i18nScope = createI18nScope({
|
|
168
226
|
locale: untrack(() => locale),
|
|
169
|
-
provider: untrack(() =>
|
|
227
|
+
provider: untrack(() => resolvedI18nProvider),
|
|
170
228
|
onLocaleChange: updateBoundLocale,
|
|
171
229
|
});
|
|
172
230
|
provideI18nScope(i18nScope);
|
|
173
231
|
const translation = useTranslation();
|
|
174
232
|
|
|
175
233
|
$effect.pre(() => {
|
|
176
|
-
const ownerProvider =
|
|
234
|
+
const ownerProvider = resolvedI18nProvider;
|
|
177
235
|
const ownerLocale = locale;
|
|
178
236
|
untrack(() => i18nScope.updateOwner({
|
|
179
237
|
locale: ownerLocale,
|
|
@@ -210,11 +268,23 @@
|
|
|
210
268
|
}
|
|
211
269
|
|
|
212
270
|
provideAdminContext({
|
|
213
|
-
get
|
|
214
|
-
get
|
|
215
|
-
|
|
271
|
+
get providerBundle() { return resolvedProviderBundle as ProviderBundle | undefined; },
|
|
272
|
+
get dataProvider() {
|
|
273
|
+
if (!resolvedDataProvider) throw new Error('AdminApp requires a DataProvider.');
|
|
274
|
+
return resolvedDataProvider;
|
|
275
|
+
},
|
|
276
|
+
get authProvider() { return resolvedAuthProvider ?? null; },
|
|
277
|
+
get accessControlProvider() { return resolvedAccessControlProvider ?? null; },
|
|
278
|
+
get liveProvider() { return resolvedLiveProvider; },
|
|
279
|
+
get auditLogProvider() { return resolvedAuditLogProvider ?? null; },
|
|
280
|
+
get notificationProvider() { return resolvedNotificationProvider ?? null; },
|
|
281
|
+
get chatProvider() { return resolvedChatProvider ?? null; },
|
|
282
|
+
get agentProvider() { return resolvedAgentProvider ?? null; },
|
|
283
|
+
get taskProvider() { return resolvedTaskProvider; },
|
|
216
284
|
get resources() { return resources; },
|
|
217
285
|
get routerProvider() { return scopedRouterProvider; },
|
|
286
|
+
get tenantAdapter() { return resolvedTenantAdapter; },
|
|
287
|
+
get tenant() { return tenant; },
|
|
218
288
|
});
|
|
219
289
|
const adminContext = captureAdminContext();
|
|
220
290
|
|
|
@@ -244,8 +314,8 @@
|
|
|
244
314
|
});
|
|
245
315
|
|
|
246
316
|
// Auth check
|
|
247
|
-
let isAuthenticated = $state(untrack(() => !
|
|
248
|
-
let authChecked = $state(untrack(() => !
|
|
317
|
+
let isAuthenticated = $state(untrack(() => !resolvedAuthProvider));
|
|
318
|
+
let authChecked = $state(untrack(() => !resolvedAuthProvider));
|
|
249
319
|
|
|
250
320
|
async function navigateWithinApp(path: string) {
|
|
251
321
|
await adminContext.navigate(path);
|
|
@@ -263,7 +333,7 @@
|
|
|
263
333
|
// Explicitly track route so auth is re-checked on navigation
|
|
264
334
|
const _route = route;
|
|
265
335
|
|
|
266
|
-
if (!
|
|
336
|
+
if (!resolvedAuthProvider) {
|
|
267
337
|
if (!cancelled) {
|
|
268
338
|
isAuthenticated = true;
|
|
269
339
|
authChecked = true;
|
|
@@ -271,7 +341,7 @@
|
|
|
271
341
|
return;
|
|
272
342
|
}
|
|
273
343
|
authChecked = false;
|
|
274
|
-
|
|
344
|
+
resolvedAuthProvider.check().then(result => {
|
|
275
345
|
if (cancelled) return;
|
|
276
346
|
isAuthenticated = result.authenticated;
|
|
277
347
|
authChecked = true;
|
|
@@ -293,7 +363,9 @@
|
|
|
293
363
|
</script>
|
|
294
364
|
|
|
295
365
|
<QueryClientProvider client={queryClient}>
|
|
296
|
-
{#if !
|
|
366
|
+
{#if !resolvedDataProvider}
|
|
367
|
+
<ConfigErrorScreen title="{title} — DataProvider is required" />
|
|
368
|
+
{:else if !authChecked}
|
|
297
369
|
<div class="flex h-screen items-center justify-center">
|
|
298
370
|
<div class="space-y-4 text-center">
|
|
299
371
|
<div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary mx-auto"></div>
|
|
@@ -302,7 +374,7 @@
|
|
|
302
374
|
</div>
|
|
303
375
|
{:else if route === '/login' && loginPage}
|
|
304
376
|
{@render loginPage()}
|
|
305
|
-
{:else if route === '/login' &&
|
|
377
|
+
{:else if route === '/login' && resolvedAuthProvider}
|
|
306
378
|
<LoginPage
|
|
307
379
|
{title}
|
|
308
380
|
defaultIdentifier={loginDefaults?.identifier}
|
|
@@ -310,15 +382,15 @@
|
|
|
310
382
|
loginHint={loginDefaults?.hint}
|
|
311
383
|
onSuccess={() => { isAuthenticated = true; void navigateWithinApp('/'); }}
|
|
312
384
|
/>
|
|
313
|
-
{:else if route === '/register' &&
|
|
385
|
+
{:else if route === '/register' && resolvedAuthProvider?.register}
|
|
314
386
|
<RegisterPage {title} />
|
|
315
|
-
{:else if route === '/forgot-password' &&
|
|
387
|
+
{:else if route === '/forgot-password' && resolvedAuthProvider?.forgotPassword}
|
|
316
388
|
<ForgotPasswordPage {title} />
|
|
317
|
-
{:else if route === '/update-password' &&
|
|
389
|
+
{:else if route === '/update-password' && resolvedAuthProvider?.updatePassword}
|
|
318
390
|
<UpdatePasswordPage {title} />
|
|
319
391
|
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
320
392
|
<ConfigErrorScreen title="{title} — {translation.t('common.configRequired')}" />
|
|
321
|
-
{:else if (isAuthenticated || !
|
|
393
|
+
{:else if (isAuthenticated || !resolvedAuthProvider) && isStandaloneTwoFactor}
|
|
322
394
|
<SystemPageShell {title}>
|
|
323
395
|
<LazyPage loader={loadTwoFactorAuthPage} props={{}} />
|
|
324
396
|
</SystemPageShell>
|
|
@@ -327,7 +399,7 @@
|
|
|
327
399
|
{@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
|
|
328
400
|
<ErrorComp status={standaloneErrorStatus} />
|
|
329
401
|
</SystemPageShell>
|
|
330
|
-
{:else if isAuthenticated || !
|
|
402
|
+
{:else if isAuthenticated || !resolvedAuthProvider}
|
|
331
403
|
<Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
|
|
332
404
|
{#key route + (params.resource ?? '') + (params.id ?? '') + (params.variant ?? '') + (params.columns ?? '')}
|
|
333
405
|
<div class="svadmin-page-enter">
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import '../app.css';
|
|
2
2
|
import { type Component, type Snippet } from 'svelte';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AccessControlProvider, AgentProvider, AuditLogProvider, AuthProvider, ChatProvider, DataProviderInput, I18nProvider, LiveProvider, MenuItem, NotificationProvider, ResourceDefinition, RouterProvider, TaskProvider, TenantAdapter, TenantContext, ThemeConfig, ThemeMode } from '@svadmin/core';
|
|
4
4
|
import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
|
|
5
5
|
import { type ComponentRegistry } from '../component-registry.svelte.js';
|
|
6
|
+
import type { AdminProviderBundle } from '../types.js';
|
|
6
7
|
interface Props {
|
|
7
|
-
|
|
8
|
+
/** Single provider, or named providers such as `{ default, analytics }`. */
|
|
9
|
+
dataProvider?: DataProviderInput;
|
|
10
|
+
/** Core canonical provider bundle. Existing top-level props take precedence. */
|
|
11
|
+
providerBundle?: AdminProviderBundle;
|
|
12
|
+
/** Compatibility alias for providerBundle. */
|
|
13
|
+
providers?: AdminProviderBundle;
|
|
8
14
|
authProvider?: AuthProvider;
|
|
15
|
+
accessControlProvider?: AccessControlProvider;
|
|
16
|
+
liveProvider?: LiveProvider;
|
|
17
|
+
auditLogProvider?: AuditLogProvider;
|
|
18
|
+
notificationProvider?: NotificationProvider;
|
|
19
|
+
chatProvider?: ChatProvider;
|
|
20
|
+
agentProvider?: AgentProvider;
|
|
9
21
|
taskProvider?: TaskProvider;
|
|
10
22
|
routerProvider?: RouterProvider;
|
|
23
|
+
tenantAdapter?: TenantAdapter;
|
|
24
|
+
/** Request/tree-scoped tenant. Tenant identifiers are never rendered by DevTools. */
|
|
25
|
+
tenant?: TenantContext;
|
|
11
26
|
resources: ResourceDefinition[];
|
|
12
27
|
/** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
|
|
13
28
|
locale?: string;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { captureAdminContext, useTranslation } from '@svadmin/core';
|
|
3
3
|
|
|
4
|
-
import type { AuditEntry } from '@svadmin/core';
|
|
4
|
+
import type { AuditEntry, AuditLogProvider } from '@svadmin/core';
|
|
5
5
|
import * as Sheet from './ui/sheet/index.js';
|
|
6
6
|
import { History, Loader2 } from '@lucide/svelte';
|
|
7
7
|
|
|
8
8
|
const i18n = useTranslation();
|
|
9
|
+
const adminContext = captureAdminContext();
|
|
9
10
|
|
|
10
11
|
let { open = $bindable(false), resource, recordId } = $props<{
|
|
11
12
|
open: boolean;
|
|
@@ -15,25 +16,53 @@
|
|
|
15
16
|
|
|
16
17
|
let logs = $state<AuditEntry[]>([]);
|
|
17
18
|
let isLoading = $state(false);
|
|
19
|
+
let requestEpoch = 0;
|
|
18
20
|
|
|
19
21
|
$effect(() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
}
|
|
22
|
+
const scopedProvider = adminContext.auditLogProvider;
|
|
23
|
+
const scopedResource = resource;
|
|
24
|
+
const scopedRecordId = recordId;
|
|
25
|
+
const shouldLoad = open;
|
|
26
|
+
const providerMeta = adminContext.getProviderMeta(
|
|
27
|
+
scopedResource,
|
|
28
|
+
scopedRecordId == null ? undefined : { recordId: scopedRecordId },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
clearAuditScope();
|
|
32
|
+
if (shouldLoad && scopedProvider) {
|
|
33
|
+
void loadAuditLogs(scopedProvider, scopedResource, providerMeta);
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
return cancelAuditRequest;
|
|
35
37
|
});
|
|
36
38
|
|
|
39
|
+
function cancelAuditRequest() {
|
|
40
|
+
requestEpoch += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clearAuditScope() {
|
|
44
|
+
cancelAuditRequest();
|
|
45
|
+
logs = [];
|
|
46
|
+
isLoading = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loadAuditLogs(
|
|
50
|
+
scopedProvider: AuditLogProvider,
|
|
51
|
+
scopedResource: string,
|
|
52
|
+
providerMeta: Record<string, unknown> | undefined,
|
|
53
|
+
) {
|
|
54
|
+
const epoch = requestEpoch;
|
|
55
|
+
isLoading = true;
|
|
56
|
+
try {
|
|
57
|
+
const entries = await scopedProvider.get({ resource: scopedResource, meta: providerMeta });
|
|
58
|
+
if (epoch === requestEpoch) logs = entries;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (epoch === requestEpoch) console.error('[svadmin] Failed to fetch audit logs', error);
|
|
61
|
+
} finally {
|
|
62
|
+
if (epoch === requestEpoch) isLoading = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
// Action semantic color mapping
|
|
38
67
|
function getActionColor(action: string) {
|
|
39
68
|
switch (action) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import type { AuditLog } from '@svadmin/core';
|
|
4
|
+
import { captureAdminContext } from '@svadmin/core';
|
|
5
|
+
import type { AuditLog, AuthProvider } from '@svadmin/core';
|
|
6
6
|
import { toast } from '@svadmin/core/toast';
|
|
7
7
|
import * as Table from './ui/table/index.js';
|
|
8
8
|
import { Badge } from './ui/badge/index.js';
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import { Loader2, ChevronLeft, ChevronRight, Search, FileSearch, Eye, X } from '@lucide/svelte';
|
|
12
12
|
|
|
13
13
|
const i18n = useTranslation();
|
|
14
|
-
|
|
15
|
-
const authProvider =
|
|
14
|
+
const adminContext = captureAdminContext();
|
|
15
|
+
const authProvider = $derived(adminContext.authProvider);
|
|
16
16
|
|
|
17
17
|
let logs = $state<AuditLog[]>([]);
|
|
18
18
|
let total = $state(0);
|
|
@@ -28,17 +28,31 @@
|
|
|
28
28
|
|
|
29
29
|
let requestId = 0;
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
function closeSnapshot() {
|
|
32
|
+
drawerOpen = false;
|
|
33
|
+
drawerLog = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
void authProvider;
|
|
38
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
39
|
+
closeSnapshot();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function loadLogs(scopedAuthProvider: AuthProvider | null, scopedPage: number) {
|
|
43
|
+
const currentId = ++requestId;
|
|
44
|
+
logs = [];
|
|
45
|
+
total = 0;
|
|
46
|
+
loading = true;
|
|
47
|
+
error = null;
|
|
48
|
+
|
|
49
|
+
if (!scopedAuthProvider?.getAuditLogs) {
|
|
33
50
|
error = i18n.t('settings.auditNotSupported') ?? 'Audit logs are not supported by the current AuthProvider';
|
|
34
51
|
loading = false;
|
|
35
52
|
return;
|
|
36
53
|
}
|
|
37
|
-
const currentId = ++requestId;
|
|
38
|
-
loading = true;
|
|
39
|
-
error = null;
|
|
40
54
|
try {
|
|
41
|
-
const result = await
|
|
55
|
+
const result = await scopedAuthProvider.getAuditLogs({ page: scopedPage, pageSize });
|
|
42
56
|
if (currentId !== requestId) return;
|
|
43
57
|
logs = result.data;
|
|
44
58
|
total = result.total;
|
|
@@ -51,8 +65,12 @@
|
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
$effect(() => {
|
|
54
|
-
void
|
|
55
|
-
loadLogs();
|
|
68
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
69
|
+
void loadLogs(authProvider, page);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
requestId += 1;
|
|
73
|
+
};
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
let totalPages = $derived(Math.ceil(total / pageSize) || 1);
|
|
@@ -193,7 +211,7 @@
|
|
|
193
211
|
{#if drawerOpen && drawerLog}
|
|
194
212
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
195
213
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
196
|
-
<div class="fixed inset-0 z-[100] flex justify-end bg-black/40 backdrop-blur-sm" onclick={
|
|
214
|
+
<div class="fixed inset-0 z-[100] flex justify-end bg-black/40 backdrop-blur-sm" onclick={closeSnapshot}>
|
|
197
215
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
198
216
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
199
217
|
<div class="bg-card shadow-2xl w-full max-w-2xl h-full flex flex-col animate-in slide-in-from-right duration-300" onclick={(e) => e.stopPropagation()}>
|
|
@@ -206,7 +224,7 @@
|
|
|
206
224
|
</h2>
|
|
207
225
|
<p class="text-xs text-muted-foreground mt-1">{formatDate(drawerLog.createdAt)} · {drawerLog.userName ?? drawerLog.userId}</p>
|
|
208
226
|
</div>
|
|
209
|
-
<button onclick={
|
|
227
|
+
<button onclick={closeSnapshot} class="text-muted-foreground hover:text-foreground transition p-2 rounded-lg hover:bg-muted">
|
|
210
228
|
<X class="w-5 h-5" />
|
|
211
229
|
</button>
|
|
212
230
|
</div>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
cell_getValue,
|
|
35
35
|
} from '@tanstack/table-core/static-functions';
|
|
36
36
|
|
|
37
|
-
import { captureAdminContext, useList, useDelete, useDeleteMany, getResource,
|
|
37
|
+
import { captureAdminContext, useList, useDelete, useDeleteMany, getResource, notifyWithProvider } from '@svadmin/core';
|
|
38
38
|
import type {
|
|
39
39
|
BaseRecord,
|
|
40
40
|
FieldDefinition,
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
Pagination as PaginationState,
|
|
44
44
|
Sort,
|
|
45
45
|
} from '@svadmin/core';
|
|
46
|
-
import { useCan
|
|
46
|
+
import { useCan } from '@svadmin/core';
|
|
47
47
|
import { readURLState, writeURLState } from '@svadmin/core';
|
|
48
48
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
49
49
|
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
const deleteManyMutation = deleteManyResult.mutation;
|
|
260
260
|
|
|
261
261
|
// ─── Permissions ──────────────────────────────────────────────
|
|
262
|
-
const acEnabled = $derived(!!
|
|
262
|
+
const acEnabled = $derived(!!adminContext.accessControlProvider);
|
|
263
263
|
const canCreatePerm = useCan(() => ({ resource: resourceName, action: 'create', queryOptions: { enabled: acEnabled } }));
|
|
264
264
|
const canEditPerm = useCan(() => ({ resource: resourceName, action: 'edit', queryOptions: { enabled: acEnabled } }));
|
|
265
265
|
const canDeletePerm = useCan(() => ({ resource: resourceName, action: 'delete', queryOptions: { enabled: acEnabled } }));
|
|
@@ -505,9 +505,15 @@
|
|
|
505
505
|
confirmAction = async () => {
|
|
506
506
|
try {
|
|
507
507
|
await deleteManyMutation.mutateAsync({ ids, resource: resourceName });
|
|
508
|
-
|
|
508
|
+
notifyWithProvider(
|
|
509
|
+
{ type: 'success', message: i18n.t('common.batchDeleteSuccess', { count: ids.length }) },
|
|
510
|
+
adminContext.notificationProvider,
|
|
511
|
+
);
|
|
509
512
|
} catch {
|
|
510
|
-
|
|
513
|
+
notifyWithProvider(
|
|
514
|
+
{ type: 'error', message: i18n.t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) },
|
|
515
|
+
adminContext.notificationProvider,
|
|
516
|
+
);
|
|
511
517
|
}
|
|
512
518
|
rowSelectionAtom.set({});
|
|
513
519
|
confirmOpen = false;
|