Skip to content

Commit 9247415

Browse files
committed
Improve permission policy scope selection
1 parent bc09b81 commit 9247415

4 files changed

Lines changed: 453 additions & 12 deletions

File tree

‎apps/admin-web/src/styles/panels.css‎

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
}
1919

2020
.form-grid.policy-form {
21-
grid-template-columns: 160px minmax(160px, 1fr) 110px minmax(260px, 1.4fr) auto;
21+
grid-template-columns: 160px minmax(160px, 1fr) 110px auto;
2222
}
2323

2424
.form-grid.model-form {
@@ -261,6 +261,144 @@
261261
align-items: center;
262262
}
263263

264+
.permission-scope-picker {
265+
display: grid;
266+
gap: 16px;
267+
margin: 2px 0 16px;
268+
border: 1px solid var(--border);
269+
border-radius: 7px;
270+
background: var(--surface);
271+
padding: 14px;
272+
}
273+
274+
.permission-picker-head {
275+
display: flex;
276+
align-items: flex-start;
277+
justify-content: space-between;
278+
gap: 12px;
279+
}
280+
281+
.permission-picker-head strong,
282+
.permission-picker-head span {
283+
display: block;
284+
}
285+
286+
.permission-picker-head > div > span {
287+
margin-top: 3px;
288+
color: var(--text-muted);
289+
font-size: 13px;
290+
}
291+
292+
.permission-picker-section {
293+
display: grid;
294+
gap: 10px;
295+
}
296+
297+
.permission-picker-section h2 {
298+
margin: 0;
299+
font-size: 14px;
300+
}
301+
302+
.permission-preset-grid,
303+
.permission-tool-grid {
304+
display: grid;
305+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
306+
gap: 8px;
307+
}
308+
309+
.permission-preset-button,
310+
.permission-tool-choice {
311+
min-width: 0;
312+
border: 1px solid var(--border);
313+
border-radius: 7px;
314+
background: #f8fafc;
315+
color: var(--text);
316+
padding: 10px;
317+
text-align: left;
318+
}
319+
320+
.permission-preset-button:hover,
321+
.permission-tool-choice:hover,
322+
.permission-tool-choice.selected {
323+
border-color: #87b8b2;
324+
background: var(--accent-muted);
325+
}
326+
327+
.permission-preset-button strong,
328+
.permission-preset-button span,
329+
.permission-tool-choice strong,
330+
.permission-tool-choice small {
331+
display: block;
332+
overflow-wrap: anywhere;
333+
}
334+
335+
.permission-preset-button span,
336+
.permission-tool-choice small {
337+
margin-top: 3px;
338+
color: var(--text-muted);
339+
font-size: 12px;
340+
}
341+
342+
.permission-tool-choice {
343+
display: grid;
344+
grid-template-columns: 22px minmax(0, 1fr);
345+
gap: 8px;
346+
}
347+
348+
.permission-choice-icon {
349+
display: inline-flex;
350+
width: 18px;
351+
height: 18px;
352+
align-items: center;
353+
justify-content: center;
354+
border: 1px solid var(--border-strong);
355+
border-radius: 4px;
356+
color: var(--accent-strong);
357+
}
358+
359+
.permission-tool-meta {
360+
grid-column: 2;
361+
display: flex;
362+
flex-wrap: wrap;
363+
gap: 6px;
364+
align-items: center;
365+
}
366+
367+
.scope-chip-list {
368+
display: flex;
369+
flex-wrap: wrap;
370+
gap: 8px;
371+
}
372+
373+
.scope-chip {
374+
display: inline-flex;
375+
max-width: 100%;
376+
align-items: center;
377+
gap: 6px;
378+
border: 1px solid #b8d5d0;
379+
border-radius: 999px;
380+
background: #eef8f6;
381+
color: #24423e;
382+
padding: 6px 9px;
383+
font-size: 12px;
384+
}
385+
386+
.scope-chip span {
387+
min-width: 0;
388+
overflow-wrap: anywhere;
389+
}
390+
391+
.permission-manual-scopes {
392+
display: grid;
393+
gap: 6px;
394+
}
395+
396+
.muted-line {
397+
margin: 0;
398+
color: var(--text-muted);
399+
font-size: 13px;
400+
}
401+
264402
.subsection-header {
265403
display: flex;
266404
align-items: center;

‎apps/admin-web/src/ui/i18n/translations.ts‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,29 @@ export const translations: Record<Locale, Record<string, string>> = {
228228
"permissions.created": "策略已创建",
229229
"permissions.deleted": "策略已删除",
230230
"permissions.noScopes": "无范围",
231-
"permissions.noPolicies": "暂无策略"
231+
"permissions.noPolicies": "暂无策略",
232+
"permissions.scopePickerTitle": "按工具选择权限",
233+
"permissions.scopePickerDetail": "选择能力或具体工具,系统会自动填入需要的范围和最低等级。",
234+
"permissions.quickGrants": "常用能力",
235+
"permissions.grantWorkspaceRead": "读取工作区",
236+
"permissions.grantWorkspaceReadDetail": "允许读取 VFS 文件和目录",
237+
"permissions.grantWorkspaceWrite": "编辑工作区",
238+
"permissions.grantWorkspaceWriteDetail": "允许创建、修改、移动和删除 VFS 内容",
239+
"permissions.grantSearch": "网页搜索",
240+
"permissions.grantSearchDetail": "允许使用已配置的搜索供应商",
241+
"permissions.grantMessaging": "发送消息",
242+
"permissions.grantMessagingDetail": "允许调用跨平台消息发送工具",
243+
"permissions.grantBuiltinTools": "全部内置工具",
244+
"permissions.grantBuiltinToolsDetail": "授予当前所有内置工具所需范围",
245+
"permissions.grantMcpTools": "全部 MCP 工具",
246+
"permissions.grantMcpToolsDetail": "授予当前已启用 MCP 工具所需范围",
247+
"permissions.toolGrants": "具体工具",
248+
"permissions.selectedScopes": "已选择范围",
249+
"permissions.manualScopes": "高级范围",
250+
"permissions.manualScopesPlaceholder": "可选,逗号分隔,例如 mcp:server:call",
251+
"permissions.removeScope": "移除此范围",
252+
"permissions.autoLevel": "当前 L{level}",
253+
"permissions.noTools": "暂无可选择工具"
232254
},
233255
"en-US": {
234256
"app.brandSubtitle": "Admin Console",
@@ -452,6 +474,28 @@ export const translations: Record<Locale, Record<string, string>> = {
452474
"permissions.created": "Policy created",
453475
"permissions.deleted": "Policy deleted",
454476
"permissions.noScopes": "no scopes",
455-
"permissions.noPolicies": "No policies"
477+
"permissions.noPolicies": "No policies",
478+
"permissions.scopePickerTitle": "Choose Permissions By Tool",
479+
"permissions.scopePickerDetail": "Pick capabilities or concrete tools; required scopes and minimum level are filled automatically.",
480+
"permissions.quickGrants": "Common Grants",
481+
"permissions.grantWorkspaceRead": "Read Workspace",
482+
"permissions.grantWorkspaceReadDetail": "Allow reading VFS files and directories",
483+
"permissions.grantWorkspaceWrite": "Edit Workspace",
484+
"permissions.grantWorkspaceWriteDetail": "Allow creating, updating, moving, and deleting VFS content",
485+
"permissions.grantSearch": "Web Search",
486+
"permissions.grantSearchDetail": "Allow using the configured search provider",
487+
"permissions.grantMessaging": "Send Messages",
488+
"permissions.grantMessagingDetail": "Allow cross-platform outbound messaging",
489+
"permissions.grantBuiltinTools": "All Built-in Tools",
490+
"permissions.grantBuiltinToolsDetail": "Grant scopes required by all current built-in tools",
491+
"permissions.grantMcpTools": "All MCP Tools",
492+
"permissions.grantMcpToolsDetail": "Grant scopes required by all enabled MCP tools",
493+
"permissions.toolGrants": "Concrete Tools",
494+
"permissions.selectedScopes": "Selected Scopes",
495+
"permissions.manualScopes": "Advanced Scopes",
496+
"permissions.manualScopesPlaceholder": "Optional, comma-separated, for example mcp:server:call",
497+
"permissions.removeScope": "Remove this scope",
498+
"permissions.autoLevel": "Current L{level}",
499+
"permissions.noTools": "No selectable tools"
456500
}
457501
};

‎apps/admin-web/src/ui/panels/PermissionsPanel.tsx‎

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Plus, RefreshCw, Trash2 } from "lucide-react";
22
import { useEffect, useState } from "react";
3-
import type { PermissionPolicy } from "../../api/types";
3+
import type { PermissionPolicy, ToolCatalogItem } from "../../api/types";
44
import { EmptyState } from "../EmptyState";
55
import { useI18n } from "../i18n/I18nProvider";
66
import { StatusBadge } from "../StatusBadge";
77
import { ToolbarButton } from "../ToolbarButton";
8+
import { PermissionScopePicker } from "./permissions/PermissionScopePicker";
89
import type { PanelProps } from "./types";
910

1011
const subjectTypes: PermissionPolicy["subjectType"][] = [
@@ -18,27 +19,38 @@ const subjectTypes: PermissionPolicy["subjectType"][] = [
1819
export function PermissionsPanel({ client, notify }: PanelProps) {
1920
const { t } = useI18n();
2021
const [policies, setPolicies] = useState<PermissionPolicy[]>([]);
22+
const [tools, setTools] = useState<ToolCatalogItem[]>([]);
2123
const [subjectType, setSubjectType] = useState<PermissionPolicy["subjectType"]>("platform");
2224
const [subjectId, setSubjectId] = useState("webui");
2325
const [maxLevel, setMaxLevel] = useState(4);
24-
const [scopes, setScopes] = useState("workspace:read,workspace:write,message:send");
26+
const [selectedScopes, setSelectedScopes] = useState<string[]>([
27+
"workspace:read",
28+
"workspace:write",
29+
"message:send"
30+
]);
31+
const [manualScopes, setManualScopes] = useState("");
2532

2633
async function load() {
2734
try {
28-
const result = await client.listPolicies();
29-
setPolicies(result.policies);
35+
const [policyResult, toolResult] = await Promise.all([
36+
client.listPolicies(),
37+
client.listTools()
38+
]);
39+
setPolicies(policyResult.policies);
40+
setTools(toolResult.tools);
3041
} catch (error) {
3142
notify(error instanceof Error ? error.message : "Failed to load policies", "error");
3243
}
3344
}
3445

3546
async function create() {
3647
try {
48+
const scopes = mergeScopes(selectedScopes, parseScopes(manualScopes));
3749
await client.createPolicy({
3850
subjectType,
3951
subjectId,
4052
maxLevel,
41-
scopes: scopes.split(",").map((scope) => scope.trim()).filter(Boolean)
53+
scopes
4254
});
4355
notify(t("permissions.created"), "ok");
4456
await load();
@@ -92,15 +104,20 @@ export function PermissionsPanel({ client, notify }: PanelProps) {
92104
onChange={(event) => setMaxLevel(Number(event.target.value))}
93105
/>
94106
</label>
95-
<label>
96-
{t("permissions.scopes")}
97-
<input value={scopes} onChange={(event) => setScopes(event.target.value)} />
98-
</label>
99107
<button className="primary-button" type="button" onClick={() => void create()}>
100108
<Plus size={16} />
101109
{t("common.add")}
102110
</button>
103111
</div>
112+
<PermissionScopePicker
113+
tools={tools}
114+
maxLevel={maxLevel}
115+
onMaxLevelChange={setMaxLevel}
116+
selectedScopes={selectedScopes}
117+
onSelectedScopesChange={setSelectedScopes}
118+
manualScopes={manualScopes}
119+
onManualScopesChange={setManualScopes}
120+
/>
104121
<div className="table-list">
105122
{policies.map((policy) => (
106123
<div className="table-row" key={policy.id}>
@@ -123,3 +140,14 @@ export function PermissionsPanel({ client, notify }: PanelProps) {
123140
</section>
124141
);
125142
}
143+
144+
function parseScopes(input: string): string[] {
145+
return input
146+
.split(",")
147+
.map((scope) => scope.trim())
148+
.filter(Boolean);
149+
}
150+
151+
function mergeScopes(...groups: string[][]): string[] {
152+
return Array.from(new Set(groups.flat())).sort();
153+
}

0 commit comments

Comments
 (0)