From d633560d7a18dfaae7a2fd45586051879d10bd01 Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:34:37 +0100 Subject: [PATCH] Name accounts in the connect wizard and rename them in the drawer The connect wizard asks for an optional account name (keys and sign-ins alike) and sets it once the account exists. In the connector drawer a named account shows its name with the account under it, and Rename in the account menu changes or clears the name. --- .../src/api/services/connectorsService.ts | 3 + .../src/connectors/ConnectWizard.test.tsx | 55 +++++++++ frontend/src/connectors/ConnectWizard.tsx | 30 +++++ .../src/connectors/ConnectionDrawer.test.tsx | 53 +++++++++ frontend/src/connectors/ConnectionDrawer.tsx | 108 +++++++++++++++++- frontend/src/connectors/accounts.ts | 2 + frontend/src/connectors/types.ts | 2 + frontend/src/locale/de.json | 14 ++- frontend/src/locale/en.json | 14 ++- frontend/src/locale/es.json | 14 ++- frontend/src/locale/jp.json | 14 ++- frontend/src/locale/ru.json | 14 ++- frontend/src/locale/zh-TW.json | 14 ++- frontend/src/locale/zh.json | 14 ++- 14 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 frontend/src/connectors/accounts.ts diff --git a/frontend/src/api/services/connectorsService.ts b/frontend/src/api/services/connectorsService.ts index d73aedd1..2b7b2a62 100644 --- a/frontend/src/api/services/connectorsService.ts +++ b/frontend/src/api/services/connectorsService.ts @@ -69,6 +69,9 @@ const connectorsService = { token, ), ), + /** Name an account ("Alerts bot"); an empty name clears it. */ + renameConnection: async (id: string, name: string, token: string | null) => + json(await apiClient.patch(endpoints.USER.CONNECTION(id), { name }, token)), disconnect: async (id: string, token: string | null) => json( await apiClient.post(endpoints.USER.CONNECTION_DISCONNECT(id), {}, token), diff --git a/frontend/src/connectors/ConnectWizard.test.tsx b/frontend/src/connectors/ConnectWizard.test.tsx index b8e0a698..8c4296df 100644 --- a/frontend/src/connectors/ConnectWizard.test.tsx +++ b/frontend/src/connectors/ConnectWizard.test.tsx @@ -20,6 +20,7 @@ const service = vi.hoisted(() => ({ listConnections: vi.fn(), setToolPermissions: vi.fn(), pickerToken: vi.fn(), + renameConnection: vi.fn(), })); vi.mock('../api/services/connectorsService', () => ({ default: service })); @@ -271,6 +272,60 @@ describe('ConnectWizard', () => { ); }); + it('names the account when the user gives it a name', async () => { + service.createConnection.mockResolvedValue({ + success: true, + connection: { id: 'conn-1' }, + }); + service.renameConnection.mockResolvedValue({ success: true }); + service.setup.mockResolvedValue({ success: true, tools: [], sources: [] }); + await render(base); + await typeInto( + document.body.querySelector('input[type="password"]')!, + '123:abc', + ); + await typeInto( + document.body.querySelector('#connect-account-name')!, + ' Alerts bot ', + ); + await click('settings.connectors.status.connect'); + expect(service.renameConnection).toHaveBeenCalledWith( + 'conn-1', + 'Alerts bot', + null, + ); + }); + + it('leaves an unnamed account alone', async () => { + service.createConnection.mockResolvedValue({ + success: true, + connection: { id: 'conn-1' }, + }); + service.setup.mockResolvedValue({ success: true, tools: [], sources: [] }); + await render(base); + await typeInto( + document.body.querySelector('input[type="password"]')!, + '123:abc', + ); + await click('settings.connectors.status.connect'); + expect(service.renameConnection).not.toHaveBeenCalled(); + }); + + it('names a signed-in account after the sign-in', async () => { + service.renameConnection.mockResolvedValue({ success: true }); + await render(drive); + await typeInto( + document.body.querySelector('#connect-account-name')!, + 'Work', + ); + await click('settings.connectors.wizard.signIn'); + expect(service.renameConnection).toHaveBeenCalledWith( + 'conn-drive', + 'Work', + null, + ); + }); + it('shows the default-key refusal in the form', async () => { service.createConnection.mockResolvedValue({ success: false, diff --git a/frontend/src/connectors/ConnectWizard.tsx b/frontend/src/connectors/ConnectWizard.tsx index bfe6e186..04eb8e02 100644 --- a/frontend/src/connectors/ConnectWizard.tsx +++ b/frontend/src/connectors/ConnectWizard.tsx @@ -44,6 +44,7 @@ import { } from '../preferences/preferenceSlice'; import type { AppDispatch } from '../store'; import { formatCount } from '../utils/dateTimeUtils'; +import { ACCOUNT_NAME_MAX } from './accounts'; import ConnectorIcon from './ConnectorIcon'; import CredentialForm, { credentialsComplete } from './CredentialForm'; import { loadConnectors, selectConnections } from './connectorsSlice'; @@ -107,6 +108,7 @@ export default function ConnectWizard({ initialConnectionId, ); const [credentials, setCredentials] = useState>({}); + const [accountName, setAccountName] = useState(''); const [setupValues, setSetupValues] = useState>({}); const [pending, setPending] = useState(false); const [error, setError] = useState(''); @@ -156,8 +158,22 @@ export default function ConnectWizard({ const refresh = () => dispatch(loadConnectors({ token })); + // A name tells two accounts of one service apart ("Alerts bot"). It is + // set once the account exists, so it works the same for keys and sign-ins. + const canName = + mode === 'connect' && + (connector.auth_kind === 'api_key' || connector.auth_kind === 'oauth'); + const afterSignIn = async (id: string) => { setConnectionId(id); + const name = accountName.trim(); + if (canName && name) { + try { + await connectorsService.renameConnection(id, name, token); + } catch { + // The account works without a name; it can be named in the drawer. + } + } refresh(); if (mode === 'reconnect') { setStep('done'); @@ -495,6 +511,20 @@ export default function ConnectWizard({ /> )} + {canName && ( + + setAccountName(e.target.value)} + /> + + )} ); diff --git a/frontend/src/connectors/ConnectionDrawer.test.tsx b/frontend/src/connectors/ConnectionDrawer.test.tsx index 01ccf1b0..7f7a126e 100644 --- a/frontend/src/connectors/ConnectionDrawer.test.tsx +++ b/frontend/src/connectors/ConnectionDrawer.test.tsx @@ -12,6 +12,7 @@ const connectors = vi.hoisted(() => ({ getCatalog: vi.fn(async () => ({ success: true, connectors: [] })), listConnections: vi.fn(async () => ({ success: true, connections: [] })), setup: vi.fn(), + renameConnection: vi.fn(), })); vi.mock('../api/services/connectorsService', () => ({ default: connectors })); @@ -209,6 +210,58 @@ describe('ConnectionDrawer', () => { ).not.toBeNull(); }); + const openAccountMenu = async () => { + const trigger = document.body.querySelector( + '[aria-label="settings.connectors.detail.accountMenu"]', + )!; + await act(async () => { + trigger.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true, button: 0 }), + ); + trigger.click(); + }); + }; + + it('shows the name an account was given, with the account under it', async () => { + connectors.getConnection.mockResolvedValue({ + success: true, + connection: { ...DETAIL, account_name: 'Work' }, + }); + await render(); + const text = document.body.textContent ?? ''; + expect(text).toContain('Work'); + expect(text).toContain('settings.connectors.detail.connectedAs'); + }); + + it('renames an account from its menu', async () => { + connectors.renameConnection.mockResolvedValue({ + success: true, + connection: { ...DETAIL, account_name: 'Work' }, + }); + await render(); + await openAccountMenu(); + const item = Array.from( + document.body.querySelectorAll('[role="menuitem"]'), + ).find((el) => el.textContent === 'settings.connectors.detail.rename')!; + await act(async () => item.click()); + const input = + document.body.querySelector('#rename-account')!; + const setter = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + 'value', + )!.set!; + await act(async () => { + setter.call(input, 'Work'); + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + await act(async () => button('settings.connectors.rename.save').click()); + expect(connectors.renameConnection).toHaveBeenCalledWith( + 'conn-1', + 'Work', + null, + ); + }); + it('labels synced content as Knowledge from this connection', async () => { await render(); expect(document.body.textContent).toContain( diff --git a/frontend/src/connectors/ConnectionDrawer.tsx b/frontend/src/connectors/ConnectionDrawer.tsx index fb995378..9b3a2dbc 100644 --- a/frontend/src/connectors/ConnectionDrawer.tsx +++ b/frontend/src/connectors/ConnectionDrawer.tsx @@ -1,5 +1,6 @@ import { CircleAlert, + Pencil, Plus, RefreshCw, RotateCw, @@ -20,6 +21,7 @@ import { Card } from '../components/ui/card'; import { ActionMenu, type MenuOption } from '../components/ui/dropdown-menu'; import { EmptyState } from '../components/ui/empty-state'; import { FormField } from '../components/ui/form-field'; +import { Input } from '../components/ui/input'; import { ListRow, ListRows } from '../components/ui/list-row'; import { LoadingState } from '../components/ui/loading-state'; import { Modal, ModalActions } from '../components/ui/modal'; @@ -37,6 +39,7 @@ import { showActionToast } from '../notifications/actionToastSlice'; import { selectToken } from '../preferences/preferenceSlice'; import type { AppDispatch } from '../store'; import { formatCount, formatDateTime } from '../utils/dateTimeUtils'; +import { ACCOUNT_NAME_MAX } from './accounts'; import { CapabilityBadges } from './ConnectorCard'; import ConnectorIcon from './ConnectorIcon'; import ConnectorSetupNotice from './ConnectorSetupNotice'; @@ -211,6 +214,82 @@ function RemoveConnectionModal({ ); } +/** Names an account ("Alerts bot"), so two accounts of a service read apart. */ +function RenameAccountModal({ + detail, + onClose, + onRenamed, +}: { + detail: ConnectionDetail; + onClose: () => void; + onRenamed: () => void; +}) { + const { t } = useTranslation(); + const token = useSelector(selectToken); + const [name, setName] = useState(detail.account_name ?? ''); + const [pending, setPending] = useState(false); + const [failed, setFailed] = useState(false); + + const save = () => { + setPending(true); + setFailed(false); + connectorsService + .renameConnection(detail.id, name.trim(), token) + .then((data) => { + if (!data?.success) throw new Error('rename failed'); + onRenamed(); + }) + .catch(() => setFailed(true)) + .finally(() => setPending(false)); + }; + + return ( + !open && onClose()} + title={t('settings.connectors.rename.title')} + description={t('settings.connectors.rename.description')} + footer={ + + } + > +
+ {failed && ( + + + + {t('settings.connectors.rename.failed')} + + + )} + setName(e.target.value)} + onKeyDown={(e) => { + if ( + e.key === 'Enter' && + name.trim() !== (detail.account_name ?? '') + ) + save(); + }} + /> +
+
+ ); +} + /** A connection tool's on/off switch: the one place it is turned on or off. */ function ToolSwitch({ tool, @@ -244,6 +323,7 @@ function AccountSection({ onReconnect, onDisconnect, onRemove, + onRename, onSyncMore, onRefreshTools, onToggleTool, @@ -255,6 +335,7 @@ function AccountSection({ onReconnect: (detail: ConnectionDetail) => void; onDisconnect: (detail: ConnectionDetail) => void; onRemove: (detail: ConnectionDetail) => void; + onRename: (detail: ConnectionDetail) => void; onSyncMore: (detail: ConnectionDetail) => void; onRefreshTools: (detail: ConnectionDetail) => Promise; /** Turns a tool of this connection on or off for agents and chat. */ @@ -276,7 +357,13 @@ function AccountSection({ detail.status === 'connected'; const canSync = connector.setup.sync !== 'off' && !!connector.sync_ingestor; const isMcp = detail.tools.some((tool) => tool.name === 'mcp_tool'); - const menu: MenuOption[] = []; + const menu: MenuOption[] = [ + { + icon: Pencil, + label: t('settings.connectors.detail.rename'), + onClick: () => onRename(detail), + }, + ]; if (detail.status !== 'disconnected') { menu.push({ icon: Unplug, @@ -303,9 +390,11 @@ function AccountSection({ {detail.last_error} ) : undefined } @@ -532,6 +621,7 @@ export default function ConnectionDrawer({ null, ); const [toRemove, setToRemove] = useState(null); + const [toRename, setToRename] = useState(null); const keys = [connector?.key, ...parts.map((part) => part.key)]; const accountIds = connections @@ -807,6 +897,7 @@ export default function ConnectionDrawer({ onReconnect={reconnect} onDisconnect={setToDisconnect} onRemove={setToRemove} + onRename={setToRename} onSyncMore={(d) => onConnect(connector, { mode: 'sync', @@ -856,6 +947,7 @@ export default function ConnectionDrawer({ onReconnect={reconnect} onDisconnect={setToDisconnect} onRemove={setToRemove} + onRename={setToRename} onSyncMore={(d) => onConnect(part, { mode: 'sync', connectionId: d.id }) } @@ -886,6 +978,16 @@ export default function ConnectionDrawer({ submitLabel={t('settings.connectors.detail.disconnect')} variant="destructive" /> + {toRename && ( + setToRename(null)} + onRenamed={() => { + setToRename(null); + refresh(); + }} + /> + )} {toRemove && ( /getUpdates." + }, + "rename": { + "title": "Konto umbenennen", + "description": "Der Name unterscheidet dieses Konto von deinen anderen Konten desselben Dienstes, für dich und die KI. Lass ihn leer, um das Konto selbst anzuzeigen.", + "label": "Kontoname", + "save": "Speichern", + "failed": "Das Konto konnte nicht umbenannt werden. Versuche es erneut." } } }, diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 904cc7cb..16ff04e8 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -1207,7 +1207,8 @@ "broken": "This connection stopped working. Reconnect to try again.", "addTools": "Add tools", "noTools": "No tools from this connection yet.", - "addToolsFailed": "Could not add the tools. Try again." + "addToolsFailed": "Could not add the tools. Try again.", + "rename": "Rename" }, "permission": { "always": "Allow", @@ -1287,7 +1288,9 @@ "addToolsDescription": { "read": "Agents can look things up. They cannot change anything.", "readWrite": "Agents can look things up and make changes. Changes need your approval." - } + }, + "accountName": "Name this account", + "accountNameHint": "Optional. Tells your accounts apart, for you and the AI, e.g. Alerts bot." }, "remove": { "title": "Remove {{name}}?", @@ -1368,6 +1371,13 @@ }, "fieldHints": { "telegram_chat_id": "Optional. Messages go to this chat, and the AI cannot pick another. Add the bot to the chat, send it a message, then find the chat's id in https://api.telegram.org/bot/getUpdates." + }, + "rename": { + "title": "Rename account", + "description": "The name tells this account apart from your other accounts of the same service, for you and the AI. Leave it empty to show the account itself.", + "label": "Account name", + "save": "Save", + "failed": "Could not rename the account. Try again." } } }, diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index c69d4449..c1be3359 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -1201,7 +1201,8 @@ "broken": "Esta conexión dejó de funcionar. Reconecta para intentarlo de nuevo.", "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." + "addToolsFailed": "No se pudieron añadir las herramientas. Inténtalo de nuevo.", + "rename": "Cambiar nombre" }, "permission": { "always": "Permitir", @@ -1281,7 +1282,9 @@ "addToolsDescription": { "read": "Los agentes pueden consultar información. No pueden cambiar nada.", "readWrite": "Los agentes pueden consultar información y hacer cambios. Los cambios necesitan tu aprobación." - } + }, + "accountName": "Nombre de esta cuenta", + "accountNameHint": "Opcional. Distingue tus cuentas, para ti y para la IA; por ejemplo, Bot de alertas." }, "remove": { "title": "¿Eliminar {{name}}?", @@ -1362,6 +1365,13 @@ }, "fieldHints": { "telegram_chat_id": "Opcional. Los mensajes van a este chat y la IA no puede elegir otro. Añade el bot al chat, envíale un mensaje y busca el id del chat en https://api.telegram.org/bot/getUpdates." + }, + "rename": { + "title": "Cambiar el nombre de la cuenta", + "description": "El nombre distingue esta cuenta de tus otras cuentas del mismo servicio, para ti y para la IA. Déjalo vacío para mostrar la cuenta en sí.", + "label": "Nombre de la cuenta", + "save": "Guardar", + "failed": "No se pudo cambiar el nombre de la cuenta. Inténtalo de nuevo." } } }, diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 8d5aaae9..01ad25a7 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -1192,7 +1192,8 @@ "broken": "この接続は動作しなくなりました。再接続してもう一度お試しください。", "addTools": "ツールを追加", "noTools": "この接続のツールはまだありません。", - "addToolsFailed": "ツールを追加できませんでした。もう一度お試しください。" + "addToolsFailed": "ツールを追加できませんでした。もう一度お試しください。", + "rename": "名前を変更" }, "permission": { "always": "許可", @@ -1267,7 +1268,9 @@ "addToolsDescription": { "read": "エージェントは情報を参照できます。変更はできません。", "readWrite": "エージェントは情報の参照と変更ができます。変更にはあなたの承認が必要です。" - } + }, + "accountName": "このアカウントの名前", + "accountNameHint": "任意。あなたと AI がアカウントを区別できるようにします。例: 通知ボット" }, "remove": { "title": "{{name}} を削除しますか?", @@ -1343,6 +1346,13 @@ }, "fieldHints": { "telegram_chat_id": "任意。メッセージはこのチャットに送られ、AI は別のチャットを選べません。ボットをチャットに追加してメッセージを送り、https://api.telegram.org/bot/getUpdates でチャットの id を確認してください。" + }, + "rename": { + "title": "アカウント名を変更", + "description": "この名前で、同じサービスの他のアカウントと区別できます(あなたと AI の両方)。空欄にするとアカウントそのものが表示されます。", + "label": "アカウント名", + "save": "保存", + "failed": "アカウント名を変更できませんでした。もう一度お試しください。" } } }, diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 13ae6189..513f6828 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -1257,7 +1257,8 @@ "broken": "Это подключение перестало работать. Переподключите, чтобы попробовать снова.", "addTools": "Добавить инструменты", "noTools": "У этого подключения пока нет инструментов.", - "addToolsFailed": "Не удалось добавить инструменты. Попробуйте ещё раз." + "addToolsFailed": "Не удалось добавить инструменты. Попробуйте ещё раз.", + "rename": "Переименовать" }, "permission": { "always": "Разрешать", @@ -1347,7 +1348,9 @@ "addToolsDescription": { "read": "Агенты могут находить информацию. Изменять ничего не могут.", "readWrite": "Агенты могут находить информацию и вносить изменения. Изменения требуют вашего одобрения." - } + }, + "accountName": "Название аккаунта", + "accountNameHint": "Необязательно. Помогает вам и ИИ различать аккаунты, например «Бот оповещений»." }, "remove": { "title": "Удалить {{name}}?", @@ -1438,6 +1441,13 @@ }, "fieldHints": { "telegram_chat_id": "Необязательно. Сообщения уходят в этот чат, и ИИ не может выбрать другой. Добавьте бота в чат, отправьте ему сообщение и найдите id чата на https://api.telegram.org/bot/getUpdates." + }, + "rename": { + "title": "Переименовать аккаунт", + "description": "Название отличает этот аккаунт от других ваших аккаунтов того же сервиса, для вас и для ИИ. Оставьте поле пустым, чтобы показывать сам аккаунт.", + "label": "Название аккаунта", + "save": "Сохранить", + "failed": "Не удалось переименовать аккаунт. Попробуйте ещё раз." } } }, diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index c354531d..a408a0fb 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -1192,7 +1192,8 @@ "broken": "此連線已停止運作。重新連線以再試一次。", "addTools": "新增工具", "noTools": "此連線還沒有工具。", - "addToolsFailed": "無法新增工具。請再試一次。" + "addToolsFailed": "無法新增工具。請再試一次。", + "rename": "重新命名" }, "permission": { "always": "允許", @@ -1267,7 +1268,9 @@ "addToolsDescription": { "read": "代理可以查詢資訊,但無法做任何變更。", "readWrite": "代理可以查詢資訊並進行變更。變更需要你的核准。" - } + }, + "accountName": "為此帳戶命名", + "accountNameHint": "選填。讓你和 AI 區分你的帳戶,例如「警示機器人」。" }, "remove": { "title": "要移除 {{name}} 嗎?", @@ -1343,6 +1346,13 @@ }, "fieldHints": { "telegram_chat_id": "選填。訊息會傳送到此聊天,AI 無法選擇其他聊天。將機器人加入聊天並傳送一則訊息給它,然後在 https://api.telegram.org/bot/getUpdates 中找到該聊天的 id。" + }, + "rename": { + "title": "重新命名帳戶", + "description": "此名稱可讓你和 AI 將此帳戶與同一服務的其他帳戶區分開來。留空則顯示帳戶本身。", + "label": "帳戶名稱", + "save": "儲存", + "failed": "無法重新命名帳戶。請再試一次。" } } }, diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 887c7173..50737a75 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -1192,7 +1192,8 @@ "broken": "此连接已停止工作。重新连接以再试一次。", "addTools": "添加工具", "noTools": "此连接还没有工具。", - "addToolsFailed": "无法添加工具。请重试。" + "addToolsFailed": "无法添加工具。请重试。", + "rename": "重命名" }, "permission": { "always": "允许", @@ -1267,7 +1268,9 @@ "addToolsDescription": { "read": "智能体可以查询信息,但不能做任何更改。", "readWrite": "智能体可以查询信息并进行更改。更改需要你的批准。" - } + }, + "accountName": "为此账户命名", + "accountNameHint": "可选。让你和 AI 区分你的账户,例如“告警机器人”。" }, "remove": { "title": "移除 {{name}}?", @@ -1343,6 +1346,13 @@ }, "fieldHints": { "telegram_chat_id": "可选。消息会发送到此聊天,AI 无法选择其他聊天。将机器人加入聊天并给它发一条消息,然后在 https://api.telegram.org/bot/getUpdates 中找到该聊天的 id。" + }, + "rename": { + "title": "重命名账户", + "description": "此名称可让你和 AI 将此账户与同一服务的其他账户区分开。留空则显示账户本身。", + "label": "账户名称", + "save": "保存", + "failed": "无法重命名账户。请重试。" } } },