Q101: a connector can hold several labelled company accounts, each granted and used by name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
736c45c0ee
commit
4a6641fd49
23 files changed
+306
-151
No files matched your search
@@ -3,6 +3,7 @@ import type { AppSnapshot } from "../../../shared/domain";
|
||||
import { Button } from "../shared/ui";
|
||||
import { ToolkitCatalog } from "../pages/connectors/ToolkitCatalog";
|
||||
import { useConnectorCatalog } from "../pages/connectors/useConnectorCatalog";
|
||||
import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands";
|
||||
import type { SnapshotActions } from "./useSnapshot";
|
||||
import { t } from "../i18n/i18n";
|
||||
|
||||
@@ -60,7 +61,7 @@ export function ConnectorsStep({
|
||||
entries={catalog.entries}
|
||||
connectedSlugs={connectedSlugs}
|
||||
busySlug={busySlug}
|
||||
onConnect={connect}
|
||||
onConnect={(slug) => connect(slug, DEFAULT_CONNECTOR_ACCOUNT_LABEL)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -383,6 +383,9 @@ export const zh: Record<string, string> = {
|
||||
"Recently decided": "最近已决",
|
||||
"Reconnect": "重新连接",
|
||||
"Reconnect waiting for authorization": "重新连接等待授权",
|
||||
"Service": "服务",
|
||||
"Account label, e.g. sales": "账号标签,例如 sales",
|
||||
"Add account": "添加账号",
|
||||
"Refresh models": "刷新模型",
|
||||
"Region": "区域",
|
||||
"Rejected": "已驳回",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { activeOrgId } from "./types";
|
||||
import { ConnectedList } from "./connectors/ConnectedList";
|
||||
import { ToolkitCatalog } from "./connectors/ToolkitCatalog";
|
||||
import { useConnectorCatalog } from "./connectors/useConnectorCatalog";
|
||||
import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands";
|
||||
|
||||
/**
|
||||
* The company's connector control room: what is connected, who holds it, and
|
||||
@@ -66,7 +67,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
logoBySlug={logoBySlug}
|
||||
busySlug={busySlug}
|
||||
busyAction={busyAction}
|
||||
onReconnect={connect}
|
||||
onConnect={connect}
|
||||
onDelete={disconnect}
|
||||
/>
|
||||
|
||||
@@ -93,7 +94,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
entries={catalog.entries}
|
||||
connectedSlugs={connectedSlugs}
|
||||
busySlug={busySlug}
|
||||
onConnect={connect}
|
||||
onConnect={(slug) => connect(slug, DEFAULT_CONNECTOR_ACCOUNT_LABEL)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Button, IconButton, Pill } from "../../shared/ui";
|
||||
import type { AgentRecord, ConnectorAccountRecord, ConnectorGrantRecord } from "../../../../shared/domain";
|
||||
import { ToolkitTile } from "./ToolkitCatalog";
|
||||
@@ -26,7 +27,7 @@ export function ConnectedList({
|
||||
logoBySlug,
|
||||
busySlug,
|
||||
busyAction,
|
||||
onReconnect,
|
||||
onConnect,
|
||||
onDelete,
|
||||
}: {
|
||||
accounts: ConnectorAccountRecord[];
|
||||
@@ -35,18 +36,26 @@ export function ConnectedList({
|
||||
logoBySlug: Map<string, string | null>;
|
||||
busySlug: string | null;
|
||||
busyAction: "connect" | "disconnect" | null;
|
||||
onReconnect: (toolkitSlug: string) => void;
|
||||
onDelete: (toolkitSlug: string) => void;
|
||||
onConnect: (toolkitSlug: string, label: string) => void;
|
||||
onDelete: (toolkitSlug: string, label: string) => void;
|
||||
}) {
|
||||
const [addSlug, setAddSlug] = useState("");
|
||||
const [addLabel, setAddLabel] = useState("");
|
||||
if (accounts.length === 0) return null;
|
||||
const agentNames = new Map(agents.map((agent) => [agent.id, agent.name]));
|
||||
const toolkits = [...new Map(accounts.map((account) => [account.toolkitSlug, { slug: account.toolkitSlug, name: account.toolkitName }])).values()];
|
||||
const addAccount = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
onConnect(addSlug, addLabel);
|
||||
setAddLabel("");
|
||||
};
|
||||
return (
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<div className="section-label" style={{ marginBottom: 12 }}>{t("Connected")}</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{accounts.map((account) => {
|
||||
const holders = grants
|
||||
.filter((grant) => grant.toolkitSlug === account.toolkitSlug)
|
||||
.filter((grant) => grant.toolkitSlug === account.toolkitSlug && grant.accountLabel === account.label)
|
||||
.map((grant) => agentNames.get(grant.agentId))
|
||||
.filter((name): name is string => Boolean(name));
|
||||
const busy = busySlug !== null;
|
||||
@@ -60,17 +69,18 @@ export function ConnectedList({
|
||||
size={30}
|
||||
/>
|
||||
<span style={{ fontWeight: 600, fontSize: 13.5 }}>{account.toolkitName}</span>
|
||||
<span style={{ color: "var(--text-2)", fontSize: 12.5 }}>{account.label}</span>
|
||||
<Pill tone={STATUS_TONE[account.status]} dot>{account.status.charAt(0) + account.status.slice(1).toLowerCase()}</Pill>
|
||||
{account.pendingAccountId && <Pill tone="amber" dot>{t("Reconnect waiting for authorization")}</Pill>}
|
||||
<span className="row" style={{ gap: 6, marginLeft: "auto" }}>
|
||||
<Button size="small" disabled={busy} onClick={() => onReconnect(account.toolkitSlug)}>
|
||||
<Button size="small" disabled={busy} onClick={() => onConnect(account.toolkitSlug, account.label)}>
|
||||
{busySlug === account.toolkitSlug && busyAction === "connect" ? t("Opening…") : t("Reconnect")}
|
||||
</Button>
|
||||
<IconButton
|
||||
icon="x"
|
||||
label={t("Delete {name} connection", { name: account.toolkitName })}
|
||||
label={t("Delete {name} connection", { name: `${account.toolkitName} ${account.label}` })}
|
||||
disabled={busy}
|
||||
onClick={() => onDelete(account.toolkitSlug)}
|
||||
onClick={() => onDelete(account.toolkitSlug, account.label)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -83,6 +93,14 @@ export function ConnectedList({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<form className="row" style={{ gap: 8, marginTop: 12 }} onSubmit={addAccount}>
|
||||
<select className="input" style={{ width: 180 }} value={addSlug} onChange={(event) => setAddSlug(event.target.value)} required>
|
||||
<option value="" disabled>{t("Service")}</option>
|
||||
{toolkits.map((toolkit) => <option key={toolkit.slug} value={toolkit.slug}>{toolkit.name}</option>)}
|
||||
</select>
|
||||
<input className="input" style={{ width: 200 }} placeholder={t("Account label, e.g. sales")} value={addLabel} onChange={(event) => setAddLabel(event.target.value)} required maxLength={40} />
|
||||
<Button size="small" type="submit" disabled={busySlug !== null}>{t("Add account")}</Button>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -22,8 +22,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
|
||||
busySlug: string | null;
|
||||
busyAction: "connect" | "disconnect" | null;
|
||||
connectError: string | null;
|
||||
connect: (toolkitSlug: string) => void;
|
||||
disconnect: (toolkitSlug: string) => void;
|
||||
connect: (toolkitSlug: string, label: string) => void;
|
||||
disconnect: (toolkitSlug: string, label: string) => void;
|
||||
retry: () => void;
|
||||
} {
|
||||
const [catalogSlot, setCatalogSlot] = useState<CatalogSlot>(null);
|
||||
@@ -75,13 +75,13 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
|
||||
[actions, orgId],
|
||||
);
|
||||
|
||||
const pollUntilActive = useCallback((toolkitSlug: string) => {
|
||||
const pollUntilActive = useCallback((toolkitSlug: string, label: string) => {
|
||||
const poll = (attempt: number) => {
|
||||
if (attempt >= CONNECT_POLL_ATTEMPTS) return;
|
||||
pollTimer.current = setTimeout(() => {
|
||||
void refreshAccounts().then((data) => {
|
||||
const account = data?.connectorAccounts.find(
|
||||
(item) => item.orgId === orgId && item.toolkitSlug === toolkitSlug,
|
||||
(item) => item.orgId === orgId && item.toolkitSlug === toolkitSlug && item.label === label,
|
||||
);
|
||||
if (account?.status === "ACTIVE" && !account.pendingAccountId) return;
|
||||
poll(attempt + 1);
|
||||
@@ -91,17 +91,17 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
|
||||
poll(0);
|
||||
}, [refreshAccounts, orgId]);
|
||||
|
||||
const connect = useCallback((toolkitSlug: string) => {
|
||||
const connect = useCallback((toolkitSlug: string, label: string) => {
|
||||
setBusy({ slug: toolkitSlug, action: "connect" });
|
||||
setConnectError(null);
|
||||
void window.agentCompany
|
||||
.connectComposioToolkit({ orgId, toolkitSlug })
|
||||
.connectComposioToolkit({ orgId, toolkitSlug, label })
|
||||
.then(async (result) => {
|
||||
if (!result.ok) {
|
||||
setConnectError(result.error.message);
|
||||
return;
|
||||
}
|
||||
pollUntilActive(toolkitSlug);
|
||||
pollUntilActive(toolkitSlug, label.trim().toLowerCase());
|
||||
if (result.data.redirectUrl) {
|
||||
const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl);
|
||||
if (!opened.ok) setConnectError(opened.error.message);
|
||||
@@ -110,10 +110,10 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
|
||||
.finally(() => setBusy(null));
|
||||
}, [orgId, pollUntilActive]);
|
||||
|
||||
const disconnect = useCallback((toolkitSlug: string) => {
|
||||
const disconnect = useCallback((toolkitSlug: string, label: string) => {
|
||||
setBusy({ slug: toolkitSlug, action: "disconnect" });
|
||||
setConnectError(null);
|
||||
void actions.run(() => window.agentCompany.disconnectComposioToolkit({ orgId, toolkitSlug }))
|
||||
void actions.run(() => window.agentCompany.disconnectComposioToolkit({ orgId, toolkitSlug, label }))
|
||||
.catch((error: unknown) => setConnectError(error instanceof Error ? error.message : String(error)))
|
||||
.finally(() => setBusy(null));
|
||||
}, [actions, orgId]);
|
||||
|
||||
Reference in new issue
Block a user