@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,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext
|
|
3
|
-
import type { Identity, MenuItem } from '@svadmin/core';
|
|
2
|
+
import { captureAdminContext } from '@svadmin/core';
|
|
3
|
+
import type { AccessControlProvider, Action, Identity, MenuItem } from '@svadmin/core';
|
|
4
4
|
import { getPath } from '../router-state.svelte.js';
|
|
5
5
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
6
|
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
routeMode?: 'hash' | 'path' | 'auto';
|
|
36
36
|
} = $props();
|
|
37
37
|
const adminContext = captureAdminContext();
|
|
38
|
+
const accessControlProvider = $derived(adminContext.accessControlProvider);
|
|
38
39
|
|
|
39
40
|
const effectiveRouteMode = $derived(
|
|
40
41
|
routeMode === 'auto'
|
|
@@ -103,13 +104,23 @@
|
|
|
103
104
|
items: NavItem[];
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
async function
|
|
107
|
+
async function checkAccess(provider: AccessControlProvider | null, resource: string, action: Action): Promise<boolean> {
|
|
108
|
+
if (!provider) return true;
|
|
109
|
+
|
|
110
|
+
const result = await provider.can({
|
|
111
|
+
resource,
|
|
112
|
+
action,
|
|
113
|
+
meta: adminContext.getProviderMeta(resource),
|
|
114
|
+
});
|
|
115
|
+
return Array.isArray(result) ? (result[0]?.can ?? false) : result.can;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function hasCustomMenuAccess(menuItem: MenuItem, provider: AccessControlProvider | null): Promise<boolean> {
|
|
107
119
|
const resource = menuItem.meta?.resource;
|
|
108
120
|
if (!resource) return true;
|
|
109
121
|
|
|
110
122
|
try {
|
|
111
|
-
|
|
112
|
-
return can;
|
|
123
|
+
return await checkAccess(provider, resource, menuItem.meta?.action ?? 'list');
|
|
113
124
|
} catch {
|
|
114
125
|
// Permission-provider failures must not expose protected navigation.
|
|
115
126
|
return false;
|
|
@@ -128,17 +139,17 @@
|
|
|
128
139
|
}));
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
async function visibleCustomMenuItem(menuItem: MenuItem): Promise<MenuItem | null> {
|
|
132
|
-
if (menuItem.meta?.hidden || !await hasCustomMenuAccess(menuItem)) return null;
|
|
142
|
+
async function visibleCustomMenuItem(menuItem: MenuItem, provider: AccessControlProvider | null): Promise<MenuItem | null> {
|
|
143
|
+
if (menuItem.meta?.hidden || !await hasCustomMenuAccess(menuItem, provider)) return null;
|
|
133
144
|
if (!menuItem.children) return menuItem;
|
|
134
145
|
|
|
135
|
-
const children = await visibleCustomMenuItems(menuItem.children);
|
|
146
|
+
const children = await visibleCustomMenuItems(menuItem.children, provider);
|
|
136
147
|
if (children.length === 0 && !menuItem.href) return null;
|
|
137
148
|
return { ...menuItem, children };
|
|
138
149
|
}
|
|
139
150
|
|
|
140
|
-
async function visibleCustomMenuItems(menuItems: MenuItem[]): Promise<MenuItem[]> {
|
|
141
|
-
const filteredItems = await Promise.all(menuItems.map(visibleCustomMenuItem));
|
|
151
|
+
async function visibleCustomMenuItems(menuItems: MenuItem[], provider: AccessControlProvider | null): Promise<MenuItem[]> {
|
|
152
|
+
const filteredItems = await Promise.all(menuItems.map((menuItem) => visibleCustomMenuItem(menuItem, provider)));
|
|
142
153
|
return filteredItems.filter(isVisibleCustomMenuItem);
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -146,13 +157,12 @@
|
|
|
146
157
|
|
|
147
158
|
$effect(() => {
|
|
148
159
|
const configuredMenu = menu?.length ? snapshotCustomMenuItems(menu) : [];
|
|
149
|
-
|
|
150
|
-
getAccessControlProvider();
|
|
160
|
+
const provider = accessControlProvider;
|
|
151
161
|
customMenuItems = [];
|
|
152
162
|
if (configuredMenu.length === 0) return;
|
|
153
163
|
|
|
154
164
|
let cancelled = false;
|
|
155
|
-
void visibleCustomMenuItems(configuredMenu).then((visibleItems) => {
|
|
165
|
+
void visibleCustomMenuItems(configuredMenu, provider).then((visibleItems) => {
|
|
156
166
|
if (!cancelled) customMenuItems = visibleItems;
|
|
157
167
|
});
|
|
158
168
|
|
|
@@ -163,13 +173,13 @@
|
|
|
163
173
|
|
|
164
174
|
$effect(() => {
|
|
165
175
|
const homeLabel = i18n.t('common.home');
|
|
166
|
-
const currentResources =
|
|
176
|
+
const currentResources = adminContext.resources;
|
|
177
|
+
const provider = accessControlProvider;
|
|
167
178
|
let cancelled = false;
|
|
168
179
|
|
|
169
180
|
Promise.all(currentResources.map(async (r) => {
|
|
170
181
|
try {
|
|
171
|
-
|
|
172
|
-
return { r, can };
|
|
182
|
+
return { r, can: await checkAccess(provider, r.name, 'list') };
|
|
173
183
|
} catch {
|
|
174
184
|
return { r, can: false };
|
|
175
185
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Input } from './ui/input/index.js';
|
|
3
|
+
import { captureAdminContext } from '@svadmin/core';
|
|
3
4
|
import type { ChatProvider } from '@svadmin/core';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -29,11 +30,14 @@
|
|
|
29
30
|
name,
|
|
30
31
|
type = 'text'
|
|
31
32
|
}: Props = $props();
|
|
33
|
+
const adminContext = captureAdminContext();
|
|
32
34
|
|
|
33
35
|
let suggestion = $state('');
|
|
34
36
|
let isPredicting = $state(false);
|
|
35
37
|
let abortController: AbortController | null = null;
|
|
36
38
|
let predictTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
|
+
let requestEpoch = 0;
|
|
40
|
+
let observedValue = value;
|
|
37
41
|
|
|
38
42
|
// The ghost text shows the suggestion, but only the part *after* what the user has typed
|
|
39
43
|
// It only displays if the suggestion actually starts with the current value
|
|
@@ -49,66 +53,102 @@
|
|
|
49
53
|
return '';
|
|
50
54
|
});
|
|
51
55
|
|
|
52
|
-
import { getChatProvider } from '@svadmin/core';
|
|
53
|
-
|
|
54
56
|
const suffixText = $derived(ghostText ? ghostText.slice(value.length) : '');
|
|
57
|
+
const resolvedProvider = $derived(provider ?? adminContext.chatProvider);
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
$effect(() => {
|
|
60
|
+
void resolvedProvider;
|
|
61
|
+
void context;
|
|
62
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
63
|
+
clearSuggestionScope();
|
|
64
|
+
|
|
65
|
+
return cancelPrediction;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
$effect(() => {
|
|
69
|
+
const currentValue = value;
|
|
70
|
+
if (currentValue !== observedValue) {
|
|
71
|
+
observedValue = currentValue;
|
|
72
|
+
clearSuggestionScope();
|
|
61
73
|
}
|
|
62
|
-
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function cancelPrediction() {
|
|
77
|
+
requestEpoch += 1;
|
|
78
|
+
abortController?.abort();
|
|
79
|
+
abortController = null;
|
|
80
|
+
if (predictTimer) clearTimeout(predictTimer);
|
|
81
|
+
predictTimer = null;
|
|
82
|
+
isPredicting = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clearSuggestionScope() {
|
|
86
|
+
cancelPrediction();
|
|
87
|
+
suggestion = '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isCurrentPrediction(epoch: number, scopedValue: string) {
|
|
91
|
+
return epoch === requestEpoch && value === scopedValue;
|
|
92
|
+
}
|
|
63
93
|
|
|
64
94
|
async function predict() {
|
|
65
|
-
|
|
95
|
+
predictTimer = null;
|
|
96
|
+
const scopedProvider = resolvedProvider;
|
|
97
|
+
const scopedValue = value;
|
|
98
|
+
const scopedContext = context;
|
|
99
|
+
if (!scopedProvider || !scopedValue.trim()) {
|
|
66
100
|
suggestion = '';
|
|
67
101
|
return;
|
|
68
102
|
}
|
|
69
103
|
|
|
104
|
+
cancelPrediction();
|
|
105
|
+
const epoch = requestEpoch;
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
abortController = controller;
|
|
70
108
|
isPredicting = true;
|
|
71
|
-
|
|
72
|
-
abortController.abort();
|
|
73
|
-
}
|
|
74
|
-
abortController = new AbortController();
|
|
109
|
+
suggestion = '';
|
|
75
110
|
|
|
76
111
|
try {
|
|
77
|
-
const prompt = `You are an autocomplete engine. The user is typing: "${
|
|
78
|
-
Context: ${
|
|
112
|
+
const prompt = `You are an autocomplete engine. The user is typing: "${scopedValue}".
|
|
113
|
+
Context: ${scopedContext}.
|
|
79
114
|
Provide ONLY the most likely completion of the exact phrase the user is typing.
|
|
80
115
|
Include the user's input in your returned string.
|
|
81
116
|
Do not include any other text, Markdown formatting, quotes, or explanations.`;
|
|
82
117
|
|
|
83
|
-
const result =
|
|
118
|
+
const result = scopedProvider.sendMessage(
|
|
84
119
|
[{ id: 'prompt', role: 'user', content: prompt, timestamp: Date.now() }],
|
|
85
|
-
{ signal:
|
|
120
|
+
{ signal: controller.signal }
|
|
86
121
|
);
|
|
87
122
|
|
|
88
|
-
suggestion = '';
|
|
89
|
-
|
|
90
|
-
// Handle streaming or Promise responses seamlessly
|
|
91
123
|
if (Symbol.asyncIterator in result) {
|
|
92
124
|
for await (const chunk of result as AsyncIterable<string>) {
|
|
125
|
+
if (!isCurrentPrediction(epoch, scopedValue)) return;
|
|
93
126
|
suggestion += chunk;
|
|
94
127
|
}
|
|
95
128
|
} else {
|
|
96
|
-
|
|
129
|
+
const response = await result;
|
|
130
|
+
if (isCurrentPrediction(epoch, scopedValue)) suggestion = response;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isCurrentPrediction(epoch, scopedValue)) {
|
|
134
|
+
suggestion = suggestion.trim().replace(/^["']|["']$/g, '');
|
|
97
135
|
}
|
|
98
|
-
|
|
99
|
-
// Clean up the suggestion
|
|
100
|
-
suggestion = suggestion.trim().replace(/^["']|["']$/g, '');
|
|
101
136
|
} catch (err: unknown) {
|
|
102
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
137
|
+
if (isCurrentPrediction(epoch, scopedValue) && err instanceof Error && err.name !== 'AbortError') {
|
|
103
138
|
suggestion = '';
|
|
104
139
|
}
|
|
105
140
|
} finally {
|
|
106
|
-
|
|
141
|
+
if (isCurrentPrediction(epoch, scopedValue)) {
|
|
142
|
+
isPredicting = false;
|
|
143
|
+
if (abortController === controller) abortController = null;
|
|
144
|
+
}
|
|
107
145
|
}
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
function handleInput(e: Event) {
|
|
111
149
|
const target = e.target as HTMLInputElement;
|
|
150
|
+
cancelPrediction();
|
|
151
|
+
observedValue = target.value;
|
|
112
152
|
value = target.value;
|
|
113
153
|
|
|
114
154
|
// Reset suggestion immediately if it no longer matches
|
|
@@ -116,19 +156,18 @@ Do not include any other text, Markdown formatting, quotes, or explanations.`;
|
|
|
116
156
|
suggestion = '';
|
|
117
157
|
}
|
|
118
158
|
|
|
119
|
-
if (predictTimer) clearTimeout(predictTimer);
|
|
120
|
-
|
|
121
|
-
// Debounce prediction
|
|
122
159
|
predictTimer = setTimeout(() => {
|
|
123
|
-
predict();
|
|
160
|
+
void predict();
|
|
124
161
|
}, 500);
|
|
125
162
|
}
|
|
126
163
|
|
|
127
164
|
function handleKeyDown(e: KeyboardEvent) {
|
|
128
165
|
if (e.key === 'Tab' && suffixText) {
|
|
129
|
-
e.preventDefault();
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
cancelPrediction();
|
|
168
|
+
observedValue = ghostText;
|
|
130
169
|
value = ghostText;
|
|
131
|
-
suggestion = '';
|
|
170
|
+
suggestion = '';
|
|
132
171
|
}
|
|
133
172
|
}
|
|
134
173
|
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
"data-slot": dataSlot = "input",
|
|
24
24
|
...restProps
|
|
25
25
|
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const isFileInput = $derived(type?.toLowerCase() === "file");
|
|
26
28
|
</script>
|
|
27
29
|
|
|
28
|
-
{#if
|
|
30
|
+
{#if isFileInput}
|
|
29
31
|
<input
|
|
30
32
|
bind:this={ref}
|
|
31
33
|
data-slot={dataSlot}
|
package/dist/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export { default as NotificationsSettings } from "./components/NotificationsSett
|
|
|
72
72
|
export { default as ApiSettings } from "./components/ApiSettings.svelte";
|
|
73
73
|
export { default as ErrorPage } from "./components/ErrorPage.svelte";
|
|
74
74
|
export { default as PermissionMatrix } from "./components/PermissionMatrix.svelte";
|
|
75
|
-
export type { RoleInfo, ResourceInfo, ActionInfo } from "./types.js";
|
|
75
|
+
export type { AdminProviderBundle, RoleInfo, ResourceInfo, ActionInfo } from "./types.js";
|
|
76
76
|
export { default as RolesSettings } from "./components/RolesSettings.svelte";
|
|
77
77
|
export { default as AuditLogViewer } from "./components/AuditLogViewer.svelte";
|
|
78
78
|
export { default as TenantSwitcher } from "./components/TenantSwitcher.svelte";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import type { I18nProvider, ProviderBundle } from '@svadmin/core';
|
|
2
|
+
/**
|
|
3
|
+
* AdminApp 的可组合 provider 配置。
|
|
4
|
+
*
|
|
5
|
+
* 旧版顶层 props 仍然可用,且在同时传入时优先于这里的同名配置。
|
|
6
|
+
* Access control 仅控制浏览器端呈现,后端仍必须独立鉴权。
|
|
7
|
+
*/
|
|
8
|
+
export type AdminProviderBundle = ProviderBundle & {
|
|
9
|
+
/** UI-only locale integration; all other fields come from Core's canonical ProviderBundle. */
|
|
10
|
+
readonly i18nProvider?: I18nProvider;
|
|
11
|
+
};
|
|
1
12
|
export type RoleInfo = {
|
|
2
13
|
code: string;
|
|
3
14
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./components/*": "./dist/components/*"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@svadmin/core": "^0.
|
|
42
|
+
"@svadmin/core": "^0.35.0",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|