From 01caae80adaeeb6f58f70dd107bc9e0747a9804b Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:47 +0100 Subject: [PATCH] Show one account at a time on a connector's page With several accounts of a service, a dropdown picks the one shown along with its knowledge and tools, instead of listing every account. The page opens on the account behind the tool it was opened from, else one that needs signing in again. --- .../src/connectors/ConnectionDrawer.test.tsx | 62 ++++++- frontend/src/connectors/ConnectionDrawer.tsx | 159 +++++++++++++++--- frontend/src/locale/de.json | 3 +- frontend/src/locale/en.json | 3 +- frontend/src/locale/es.json | 3 +- frontend/src/locale/jp.json | 3 +- frontend/src/locale/ru.json | 3 +- frontend/src/locale/zh-TW.json | 3 +- frontend/src/locale/zh.json | 3 +- frontend/src/settings/Tools.test.tsx | 14 +- frontend/src/settings/Tools.tsx | 12 +- 11 files changed, 231 insertions(+), 37 deletions(-) diff --git a/frontend/src/connectors/ConnectionDrawer.test.tsx b/frontend/src/connectors/ConnectionDrawer.test.tsx index af6c6268..6a6e680b 100644 --- a/frontend/src/connectors/ConnectionDrawer.test.tsx +++ b/frontend/src/connectors/ConnectionDrawer.test.tsx @@ -82,7 +82,7 @@ describe('ConnectionDrawer', () => { let container: HTMLDivElement; let root: Root; let store: ReturnType; - const makeStore = () => + const makeStore = (extra: Record[] = []) => configureStore({ reducer: { connectors: connectorsReducer, @@ -103,6 +103,7 @@ describe('ConnectionDrawer', () => { status: 'connected', }, { id: 'conn-gh', connector_key: 'github', status: 'connected' }, + ...extra, ], }, preference: { token: null }, @@ -125,13 +126,20 @@ describe('ConnectionDrawer', () => { container.remove(); }); - const render = async (connector: ConnectorDefinition = DRIVE) => { - store = makeStore(); + const render = async ( + connector: ConnectorDefinition = DRIVE, + options: { + extra?: Record[]; + initialConnectionId?: string; + } = {}, + ) => { + store = makeStore(options.extra); await act(async () => { root.render( @@ -145,6 +153,54 @@ describe('ConnectionDrawer', () => { document.body.querySelectorAll('button'), ).find((b) => b.textContent === text)!; + it('shows one account at a time, switched from a dropdown', async () => { + connectors.getConnection.mockImplementation(async (id: string) => ({ + success: true, + connection: + id === 'conn-2' + ? { ...DETAIL, id: 'conn-2', account_name: 'Max' } + : { ...DETAIL, account_name: 'Lena' }, + })); + await render(DRIVE, { + extra: [ + { id: 'conn-2', connector_key: 'google_drive', status: 'connected' }, + ], + initialConnectionId: 'conn-2', + }); + const text = () => document.body.textContent ?? ''; + // The account the panel was opened for, and only that one. + expect(text()).toContain('Max'); + expect(text()).not.toContain('Lena'); + + const trigger = document.body.querySelector( + '[aria-label="settings.connectors.detail.accountPicker"]', + )!; + await act(async () => { + trigger.dispatchEvent( + new PointerEvent('pointerdown', { + bubbles: true, + button: 0, + pointerType: 'mouse', + }), + ); + }); + const other = Array.from( + document.body.querySelectorAll('[role="option"]'), + ).find((o) => o.textContent?.includes('Lena'))!; + await act(async () => other.click()); + expect(text()).toContain('Lena'); + expect(text()).not.toContain('Max'); + }); + + it('has no account dropdown with a single account', async () => { + await render(); + expect( + document.body.querySelector( + '[aria-label="settings.connectors.detail.accountPicker"]', + ), + ).toBeNull(); + }); + it('turns a tool on and off from its connection page', async () => { users.updateToolStatus.mockResolvedValue({ ok: true }); await render(); diff --git a/frontend/src/connectors/ConnectionDrawer.tsx b/frontend/src/connectors/ConnectionDrawer.tsx index 38007f81..fa15ad8d 100644 --- a/frontend/src/connectors/ConnectionDrawer.tsx +++ b/frontend/src/connectors/ConnectionDrawer.tsx @@ -26,6 +26,13 @@ import { ListRow, ListRows } from '../components/ui/list-row'; import { LoadingState } from '../components/ui/loading-state'; import { Modal, ModalActions } from '../components/ui/modal'; import { SectionHeader } from '../components/ui/section-header'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../components/ui/select'; import { SettingRow, SettingRows } from '../components/ui/setting-row'; import { Switch } from '../components/ui/switch'; import { @@ -44,7 +51,11 @@ import { ACCOUNT_NAME_MAX } from './accounts'; import { CapabilityBadges } from './ConnectorCard'; import ConnectorIcon from './ConnectorIcon'; import ConnectorSetupNotice from './ConnectorSetupNotice'; -import { loadConnectors, selectConnections } from './connectorsSlice'; +import { + connectionNeedsSignIn, + loadConnectors, + selectConnections, +} from './connectorsSlice'; import { connectorDescription, connectorName, isKeyHint } from './i18n'; import ToolPermissions from './ToolPermissions'; import type { @@ -354,6 +365,58 @@ function WritesSwitch({ ); } +/** + * Which of several accounts of a service the panel shows. The rest of the + * panel is that one account: its knowledge and its tools. + */ +function AccountPicker({ + accounts, + value, + onChange, +}: { + accounts: ConnectionDetail[]; + value: string; + onChange: (id: string) => void; +}) { + const { t } = useTranslation(); + const accountTitle = useAccountTitle(); + return ( + + ); +} + +/** + * The account to show from ``accounts``: the one picked, else one that needs + * signing in again (so a problem is not hidden behind another account), else + * the first. + */ +function shownAccount( + accounts: ConnectionDetail[], + picked: string | undefined, +): ConnectionDetail | undefined { + return ( + accounts.find((account) => account.id === picked) ?? + accounts.find(connectionNeedsSignIn) ?? + accounts[0] + ); +} + function AccountSection({ connector, detail, @@ -648,12 +711,15 @@ function AccountSection({ export default function ConnectionDrawer({ connector, parts = [], + initialConnectionId, onClose, onConnect, }: { connector: ConnectorDefinition | null; /** The same service offered another way, shown here (Jira & Confluence). */ parts?: ConnectorDefinition[]; + /** The account to show first, e.g. the one behind the tool it opened from. */ + initialConnectionId?: string; onClose: () => void; onConnect: (connector: ConnectorDefinition, options?: LaunchOptions) => void; }) { @@ -670,6 +736,16 @@ export default function ConnectionDrawer({ ); const [toRemove, setToRemove] = useState(null); const [toRename, setToRename] = useState(null); + // The account shown per service (the connector and each of its parts). + const [picked, setPicked] = useState>({}); + useEffect(() => { + setPicked({}); + }, [connector?.key, initialConnectionId]); + const pickedIn = (key: string, accounts: ConnectionDetail[]) => + picked[key] ?? + (accounts.some((account) => account.id === initialConnectionId) + ? initialConnectionId + : undefined); const keys = [connector?.key, ...parts.map((part) => part.key)]; const accountIds = connections @@ -960,7 +1036,29 @@ export default function ConnectionDrawer({ /> ) : (
- {ownDetails.map((detail) => ( + {ownDetails.length > 1 && ( + + setPicked((state) => ({ + ...state, + [connector.key]: id, + })) + } + /> + )} + {[ + shownAccount( + ownDetails, + pickedIn(connector.key, ownDetails), + )!, + ].map((detail) => ( - {partDetails.map((detail) => ( - - onConnect(part, { mode: 'sync', connectionId: d.id }) + {partDetails.length > 1 && ( + + setPicked((state) => ({ ...state, [part.key]: id })) } - onRefreshTools={refreshTools} - onToggleTool={toggleTool} - onSyncNow={syncNow} - onAddTools={addTools} - onSwitchWrites={switchWrites} /> - ))} + )} + {partDetails + .filter( + (detail) => + detail.id === + shownAccount( + partDetails, + pickedIn(part.key, partDetails), + )?.id, + ) + .map((detail) => ( + + onConnect(part, { mode: 'sync', connectionId: d.id }) + } + onRefreshTools={refreshTools} + onToggleTool={toggleTool} + onSyncNow={syncNow} + onAddTools={addTools} + onSwitchWrites={switchWrites} + /> + ))} ); })} diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 6056e006..d0a63e83 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -1202,7 +1202,8 @@ "addTools": "Werkzeuge hinzufügen", "noTools": "Noch keine Werkzeuge aus dieser Verbindung.", "addToolsFailed": "Die Werkzeuge konnten nicht hinzugefügt werden. Versuche es erneut.", - "rename": "Umbenennen" + "rename": "Umbenennen", + "accountPicker": "Konto auswählen" }, "permission": { "always": "Erlauben", diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 21fd392d..358effbf 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -1208,7 +1208,8 @@ "addTools": "Add tools", "noTools": "No tools from this connection yet.", "addToolsFailed": "Could not add the tools. Try again.", - "rename": "Rename" + "rename": "Rename", + "accountPicker": "Choose an account" }, "permission": { "always": "Allow", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 020727c1..9d7814f4 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -1202,7 +1202,8 @@ "addTools": "Añadir herramientas", "noTools": "Todavía no hay herramientas de esta conexión.", "addToolsFailed": "No se pudieron añadir las herramientas. Inténtalo de nuevo.", - "rename": "Cambiar nombre" + "rename": "Cambiar nombre", + "accountPicker": "Elegir una cuenta" }, "permission": { "always": "Permitir", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 0a64b0a7..619aeee1 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -1193,7 +1193,8 @@ "addTools": "ツールを追加", "noTools": "この接続のツールはまだありません。", "addToolsFailed": "ツールを追加できませんでした。もう一度お試しください。", - "rename": "名前を変更" + "rename": "名前を変更", + "accountPicker": "アカウントを選択" }, "permission": { "always": "許可", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 8af5e3cd..8a2f253d 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -1258,7 +1258,8 @@ "addTools": "Добавить инструменты", "noTools": "У этого подключения пока нет инструментов.", "addToolsFailed": "Не удалось добавить инструменты. Попробуйте ещё раз.", - "rename": "Переименовать" + "rename": "Переименовать", + "accountPicker": "Выберите аккаунт" }, "permission": { "always": "Разрешать", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 010e180b..64027ef1 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -1193,7 +1193,8 @@ "addTools": "新增工具", "noTools": "此連線還沒有工具。", "addToolsFailed": "無法新增工具。請再試一次。", - "rename": "重新命名" + "rename": "重新命名", + "accountPicker": "選擇帳號" }, "permission": { "always": "允許", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index f1b09845..1c72982d 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -1193,7 +1193,8 @@ "addTools": "添加工具", "noTools": "此连接还没有工具。", "addToolsFailed": "无法添加工具。请重试。", - "rename": "重命名" + "rename": "重命名", + "accountPicker": "选择账号" }, "permission": { "always": "允许", diff --git a/frontend/src/settings/Tools.test.tsx b/frontend/src/settings/Tools.test.tsx index 1dc466c3..50b633b1 100644 --- a/frontend/src/settings/Tools.test.tsx +++ b/frontend/src/settings/Tools.test.tsx @@ -106,8 +106,16 @@ vi.mock('../api/services/connectorsService', () => ({ // The connector panel itself is tested on its own; here it only has to open // for the right service. vi.mock('../connectors/ConnectionDrawer', () => ({ - default: ({ connector }: { connector: { key: string } | null }) => - connector ?
{connector.key}
: null, + default: ({ + connector, + initialConnectionId, + }: { + connector: { key: string } | null; + initialConnectionId?: string; + }) => + connector ? ( +
{`${connector.key}:${initialConnectionId}`}
+ ) : null, })); vi.mock('../connectors/useConnectorLauncher', () => ({ default: () => ({ launch: vi.fn(), modals: null }), @@ -194,7 +202,7 @@ describe('Tools page', () => { ); expect( document.body.querySelector('[data-testid="drawer"]')?.textContent, - ).toBe('telegram'); + ).toBe('telegram:conn-1'); // Stays on the Tools page. expect(document.body.querySelector('[data-testid="where"]')).toBeNull(); }); diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx index 4749a1c4..54b966f4 100644 --- a/frontend/src/settings/Tools.tsx +++ b/frontend/src/settings/Tools.tsx @@ -76,9 +76,10 @@ export default function Tools() { null, ); // The connection panel, opened here for a connected tool's service. - const [managed, setManaged] = React.useState( - null, - ); + const [managed, setManaged] = React.useState<{ + connector: ConnectorDefinition; + connectionId: string; + } | null>(null); const [mcpStatuses, setMcpStatuses] = React.useState<{ [toolId: string]: string; }>({}); @@ -262,7 +263,7 @@ export default function Tools() { const connection = connectionOf(tool); if (connection) { const connector = catalog.find((c) => c.key === connection.connector_key); - if (connector) setManaged(connector); + if (connector) setManaged({ connector, connectionId: connection.id }); else navigate( `/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`, @@ -607,7 +608,8 @@ export default function Tools() { }} /> { setManaged(null); getUserTools();