fix(ui): show group names in permission scope dropdown

Replace raw scope strings (e.g. group:nta7-goclaw:-5101523...) with
human-readable group names by fetching delivery targets from
channel_contacts via heartbeat.targets API. Scope labels are also
resolved in the permissions list display.
This commit is contained in:
viettranx committed 2026-04-02 10:37:19 +07:00
1 parent 3e4d614aa1
commit 1a2d5789d3
1 file changed
+46 -17
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useCallback } from "react";
import { Plus, Trash2, Loader2, Shield, FolderOpen, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -9,6 +9,9 @@ import {
import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
import { useConfigPermissions, type ConfigPermission } from "../hooks/use-config-permissions";
import { UserPickerCombobox } from "@/components/shared/user-picker-combobox";
import { useWs } from "@/hooks/use-ws";
import { Methods } from "@/api/protocol";
import type { DeliveryTarget } from "../hooks/use-agent-heartbeat";
const CONFIG_TYPES = [
{ value: "file_writer", label: "File Writer", descKey: "permissions.types.file_writer_desc" },
@@ -18,18 +21,26 @@ const CONFIG_TYPES = [
{ value: "*", label: "All (*)", descKey: "permissions.types.all_desc" },
];
function getScopeOptions(configType: string, existingScopes: string[]): ComboboxOption[] {
function getScopeOptions(configType: string, targets: DeliveryTarget[]): ComboboxOption[] {
// Only groups — topic-level permissions are not supported by the backend scope check
const groupOptions = targets
.filter((t) => t.kind === "group")
.map((t) => ({
value: `group:${t.channel}:${t.chatId}`,
label: t.title ? `${t.title} (${t.chatId})` : t.chatId,
}));
if (configType === "file_writer") {
const dynamic = existingScopes.map((s) => ({ value: s, label: s }));
return [
{ value: "group:*", label: "All Groups" },
...dynamic,
...groupOptions,
{ value: "*", label: "Global (*)" },
];
}
return [
{ value: "agent", label: "Agent (DM)" },
{ value: "group:*", label: "All Groups" },
...groupOptions,
{ value: "*", label: "Global (*)" },
];
}
@@ -40,6 +51,7 @@ interface AgentPermissionsTabProps {
export function AgentPermissionsTab({ agentId }: AgentPermissionsTabProps) {
const { t } = useTranslation("agents");
const ws = useWs();
const { permissions, loading, load, grant, revoke } = useConfigPermissions(agentId);
const [userId, setUserId] = useState("");
@@ -47,22 +59,39 @@ export function AgentPermissionsTab({ agentId }: AgentPermissionsTabProps) {
const [scope, setScope] = useState("group:*");
const [permission, setPermission] = useState("allow");
const [adding, setAdding] = useState(false);
const [targets, setTargets] = useState<DeliveryTarget[]>([]);
// Collect existing file_writer scopes for dynamic scope options
const existingFileWriterScopes = useMemo(() =>
[...new Set(
permissions
.filter((p) => p.configType === "file_writer")
.map((p) => p.scope)
)],
[permissions],
);
// Fetch delivery targets (groups/topics) from channel_contacts
const loadTargets = useCallback(async () => {
if (!agentId || !ws.isConnected) return;
try {
const res = await ws.call<{ targets: DeliveryTarget[] }>(
Methods.HEARTBEAT_TARGETS, { agentId },
);
setTargets(res.targets ?? []);
} catch { /* ignore — targets are optional enhancement */ }
}, [ws, agentId]);
useEffect(() => { loadTargets(); }, [loadTargets]);
const scopeOptions = useMemo(
() => getScopeOptions(configType, existingFileWriterScopes),
[configType, existingFileWriterScopes],
() => getScopeOptions(configType, targets),
[configType, targets],
);
// Build scope → display label lookup from targets
const scopeLabels = useMemo(() => {
const map = new Map<string, string>();
map.set("group:*", t("permissions.scopes.group_all"));
map.set("*", t("permissions.scopes.global"));
map.set("agent", t("permissions.scopes.agent"));
for (const tgt of targets) {
const key = `group:${tgt.channel}:${tgt.chatId}`;
map.set(key, tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId);
}
return map;
}, [targets, t]);
// Reset scope when configType changes
useEffect(() => {
if (configType === "file_writer") {
@@ -196,7 +225,7 @@ export function AgentPermissionsTab({ agentId }: AgentPermissionsTabProps) {
<div key={scopeKey}>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-muted/40">
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">{scopeKey}</span>
<span className="text-xs font-medium text-muted-foreground">{scopeLabels.get(scopeKey) ?? scopeKey}</span>
</div>
{writers.map((p) => {
const displayName = p.metadata?.displayName || p.userId;
@@ -251,7 +280,7 @@ export function AgentPermissionsTab({ agentId }: AgentPermissionsTabProps) {
</Badge>
<span className="font-medium truncate">{p.userId}</span>
<span className="text-[11px] text-muted-foreground shrink-0">{p.configType}</span>
<span className="text-[11px] text-muted-foreground shrink-0">@ {p.scope}</span>
<span className="text-[11px] text-muted-foreground shrink-0">@ {scopeLabels.get(p.scope) ?? p.scope}</span>
</div>
<Button
variant="ghost"