Q105: the Connectors page grants and revokes connector access

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 17:12:59 -04:00
1 parent 558d678ba2
commit 170b977bf8
12 files changed
+119 -11

No files matched your search

+3
View File
@@ -387,6 +387,9 @@ export const zh: Record<string, string> = {
"Account label, e.g. sales": "账号标签,例如 sales",
"Add account": "添加账号",
"limited": "受限",
"Revoke {name}": "撤销 {name}",
"Grant {name} to": "把 {name} 授予",
"Grant to…": "授予给…",
"Refresh models": "刷新模型",
"Region": "区域",
"Rejected": "已驳回",
+5 -2
View File
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { Button } from "../shared/ui";
import { t } from "../i18n/i18n";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
import { activeOrgId, requireActiveEmployee } from "./types";
import { ConnectedList } from "./connectors/ConnectedList";
import { ToolkitCatalog } from "./connectors/ToolkitCatalog";
import { useConnectorCatalog } from "./connectors/useConnectorCatalog";
@@ -15,7 +15,8 @@ import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands";
*/
export function ConnectorsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const { catalog, busySlug, busyAction, connectError, connect, disconnect, retry } = useConnectorCatalog(orgId, actions);
const employee = requireActiveEmployee(snapshot);
const { catalog, busySlug, busyAction, connectError, connect, disconnect, grant, revoke, retry } = useConnectorCatalog(orgId, actions);
const accounts = useMemo(
() => snapshot.connectorAccounts.filter((account) => account.orgId === orgId),
@@ -69,6 +70,8 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
busyAction={busyAction}
onConnect={connect}
onDelete={disconnect}
onGrant={(agentId, toolkitSlug, accountLabel) => grant(agentId, toolkitSlug, accountLabel, employee.id)}
onRevoke={revoke}
/>
{catalog.phase === "error" && (
@@ -29,6 +29,8 @@ export function ConnectedList({
busyAction,
onConnect,
onDelete,
onGrant,
onRevoke,
}: {
accounts: ConnectorAccountRecord[];
grants: ConnectorGrantRecord[];
@@ -38,6 +40,8 @@ export function ConnectedList({
busyAction: "connect" | "disconnect" | null;
onConnect: (toolkitSlug: string, label: string) => void;
onDelete: (toolkitSlug: string, label: string) => void;
onGrant: (agentId: string, toolkitSlug: string, label: string) => void;
onRevoke: (agentId: string, toolkitSlug: string) => void;
}) {
const [addSlug, setAddSlug] = useState("");
const [addLabel, setAddLabel] = useState("");
@@ -58,8 +62,10 @@ export function ConnectedList({
.filter((grant) => grant.toolkitSlug === account.toolkitSlug && grant.accountLabel === account.label)
.flatMap((grant) => {
const name = agentNames.get(grant.agentId);
return name ? [grant.allowedActions || grant.fixedArguments ? `${name} · ${t("limited")}` : name] : [];
return name ? [{ agentId: grant.agentId, text: grant.allowedActions || grant.fixedArguments ? `${name} · ${t("limited")}` : name }] : [];
});
// One account per toolkit per agent: only agents holding none of this toolkit can be granted here; the CEO holds all by role.
const grantable = agents.filter((agent) => agent.roleLevel !== "c_suite" && !grants.some((grant) => grant.agentId === agent.id && grant.toolkitSlug === account.toolkitSlug));
const busy = busySlug !== null;
return (
<div key={account.id} style={{ border: "1px solid var(--border)", borderRadius: 12, padding: 14, background: "var(--panel)" }}>
@@ -86,11 +92,27 @@ export function ConnectedList({
/>
</span>
</div>
{holders.length > 0 && (
<div className="row" style={{ gap: 6, marginTop: 12, flexWrap: "wrap" }}>
{holders.map((name) => <Pill key={name}>{name}</Pill>)}
</div>
)}
<div className="row" style={{ gap: 6, marginTop: 12, flexWrap: "wrap" }}>
{holders.map((holder) => (
<span key={holder.agentId} className="row" style={{ gap: 2 }}>
<Pill>{holder.text}</Pill>
<IconButton icon="x" size={12} label={t("Revoke {name}", { name: holder.text })} disabled={busy} onClick={() => onRevoke(holder.agentId, account.toolkitSlug)} />
</span>
))}
{account.status === "ACTIVE" && grantable.length > 0 && (
<select
className="input"
style={{ width: 160 }}
value=""
disabled={busy}
aria-label={t("Grant {name} to", { name: `${account.toolkitName} ${account.label}` })}
onChange={(event) => onGrant(event.target.value, account.toolkitSlug, account.label)}
>
<option value="" disabled>{t("Grant to…")}</option>
{grantable.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}
</select>
)}
</div>
</div>
);
})}
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { ConnectorCatalogEntry } from "../../../../shared/commands";
import type { SnapshotActions } from "../../app/useSnapshot";
import type { DesktopResult } from "../../../../shared/result";
import type { AppSnapshot } from "../../../../shared/domain";
export type CatalogState =
| { phase: "loading" }
@@ -24,6 +26,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
connectError: string | null;
connect: (toolkitSlug: string, label: string) => void;
disconnect: (toolkitSlug: string, label: string) => void;
grant: (agentId: string, toolkitSlug: string, accountLabel: string, employeeId: string) => void;
revoke: (agentId: string, toolkitSlug: string) => void;
retry: () => void;
} {
const [catalogSlot, setCatalogSlot] = useState<CatalogSlot>(null);
@@ -118,6 +122,18 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
.finally(() => setBusy(null));
}, [actions, orgId]);
// A refused grant or revoke is shown on the page, not dropped.
const runGrantChange = useCallback((change: () => Promise<DesktopResult<AppSnapshot>>) => {
setConnectError(null);
void actions.run(change).catch((error: unknown) => setConnectError(error instanceof Error ? error.message : String(error)));
}, [actions]);
const grant = useCallback((agentId: string, toolkitSlug: string, accountLabel: string, employeeId: string) => {
runGrantChange(() => window.agentCompany.grantConnector({ orgId, agentId, toolkitSlug, accountLabel, employeeId }));
}, [runGrantChange, orgId]);
const revoke = useCallback((agentId: string, toolkitSlug: string) => {
runGrantChange(() => window.agentCompany.revokeConnector({ orgId, agentId, toolkitSlug }));
}, [runGrantChange, orgId]);
return {
catalog,
busySlug: busy?.slug ?? null,
@@ -125,6 +141,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
connectError,
connect,
disconnect,
grant,
revoke,
retry: load,
};
}