Q105: the Connectors page grants and revokes connector access
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
558d678ba2
commit
170b977bf8
12 files changed
+119
-11
No files matched your search
@@ -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": "已驳回",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user