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.
This commit is contained in:
arc53-machine committed 2026-09-29 10:42:03 +01:00
1 parent bdcb88866a
commit d633560d7a
14 files changed
+334 -17

No files matched your search

@@ -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),
@@ -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<HTMLInputElement>('input[type="password"]')!,
'123:abc',
);
await typeInto(
document.body.querySelector<HTMLInputElement>('#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<HTMLInputElement>('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<HTMLInputElement>('#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,
+30
View File
@@ -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<Record<string, string>>({});
const [accountName, setAccountName] = useState('');
const [setupValues, setSetupValues] = useState<Record<string, string>>({});
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 && (
<FormField
label={t('settings.connectors.wizard.accountName')}
hint={t('settings.connectors.wizard.accountNameHint')}
>
<Input
id="connect-account-name"
autoComplete="off"
maxLength={ACCOUNT_NAME_MAX}
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
/>
</FormField>
)}
</div>
);
@@ -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<HTMLButtonElement>(
'[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<HTMLElement>('[role="menuitem"]'),
).find((el) => el.textContent === 'settings.connectors.detail.rename')!;
await act(async () => item.click());
const input =
document.body.querySelector<HTMLInputElement>('#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(
+105 -3
View File
@@ -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 (
<Modal
open
size="sm"
onOpenChange={(open) => !open && onClose()}
title={t('settings.connectors.rename.title')}
description={t('settings.connectors.rename.description')}
footer={
<ModalActions
cancelLabel={t('cancel')}
onCancel={onClose}
submitLabel={t('settings.connectors.rename.save')}
onSubmit={save}
pending={pending}
disabled={name.trim() === (detail.account_name ?? '')}
/>
}
>
<div className="flex flex-col gap-5">
{failed && (
<Alert variant="destructive">
<CircleAlert />
<AlertDescription>
{t('settings.connectors.rename.failed')}
</AlertDescription>
</Alert>
)}
<Input
id="rename-account"
label={t('settings.connectors.rename.label')}
autoComplete="off"
maxLength={ACCOUNT_NAME_MAX}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (
e.key === 'Enter' &&
name.trim() !== (detail.account_name ?? '')
)
save();
}}
/>
</div>
</Modal>
);
}
/** 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<void>;
/** 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({
<Card variant="subtle" padding="none">
<ListRows>
<ListRow
title={accountTitle(detail)}
title={detail.account_name || accountTitle(detail)}
description={
!broken && detail.last_error ? (
detail.account_name ? (
accountTitle(detail)
) : !broken && detail.last_error ? (
<span title={detail.last_error}>{detail.last_error}</span>
) : undefined
}
@@ -532,6 +621,7 @@ export default function ConnectionDrawer({
null,
);
const [toRemove, setToRemove] = useState<ConnectionDetail | null>(null);
const [toRename, setToRename] = useState<ConnectionDetail | null>(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 && (
<RenameAccountModal
detail={toRename}
onClose={() => setToRename(null)}
onRenamed={() => {
setToRename(null);
refresh();
}}
/>
)}
{toRemove && (
<RemoveConnectionModal
detail={toRemove}
+2
View File
@@ -0,0 +1,2 @@
/** The longest account name the server keeps (`ACCOUNT_NAME_MAX`). */
export const ACCOUNT_NAME_MAX = 80;
+2
View File
@@ -82,6 +82,8 @@ export type Connection = {
display_name: string | null;
icon: string;
account_label: string;
/** What the owner calls the account; `account_label` stays its identity. */
account_name?: string | null;
auth_kind: ConnectorAuthKind | null;
status: ConnectionStatus;
server_url: string | null;
+12 -2
View File
@@ -1201,7 +1201,8 @@
"broken": "Diese Verbindung funktioniert nicht mehr. Verbinde neu, um es erneut zu versuchen.",
"addTools": "Werkzeuge hinzufügen",
"noTools": "Noch keine Werkzeuge aus dieser Verbindung.",
"addToolsFailed": "Die Werkzeuge konnten nicht hinzugefügt werden. Versuche es erneut."
"addToolsFailed": "Die Werkzeuge konnten nicht hinzugefügt werden. Versuche es erneut.",
"rename": "Umbenennen"
},
"permission": {
"always": "Erlauben",
@@ -1281,7 +1282,9 @@
"addToolsDescription": {
"read": "Agenten können Dinge nachschlagen. Sie können nichts ändern.",
"readWrite": "Agenten können Dinge nachschlagen und ändern. Änderungen brauchen deine Freigabe."
}
},
"accountName": "Konto benennen",
"accountNameHint": "Optional. Unterscheidet deine Konten, für dich und die KI, z. B. Alarm-Bot."
},
"remove": {
"title": "{{name}} entfernen?",
@@ -1362,6 +1365,13 @@
},
"fieldHints": {
"telegram_chat_id": "Optional. Nachrichten gehen an diesen Chat, und die KI kann keinen anderen wählen. Füge den Bot zum Chat hinzu, sende ihm eine Nachricht und finde die ID des Chats unter https://api.telegram.org/bot<token>/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."
}
}
},
+12 -2
View File
@@ -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<token>/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."
}
}
},
+12 -2
View File
@@ -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<token>/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."
}
}
},
+12 -2
View File
@@ -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<token>/getUpdates でチャットの id を確認してください。"
},
"rename": {
"title": "アカウント名を変更",
"description": "この名前で、同じサービスの他のアカウントと区別できます(あなたと AI の両方)。空欄にするとアカウントそのものが表示されます。",
"label": "アカウント名",
"save": "保存",
"failed": "アカウント名を変更できませんでした。もう一度お試しください。"
}
}
},
+12 -2
View File
@@ -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<token>/getUpdates."
},
"rename": {
"title": "Переименовать аккаунт",
"description": "Название отличает этот аккаунт от других ваших аккаунтов того же сервиса, для вас и для ИИ. Оставьте поле пустым, чтобы показывать сам аккаунт.",
"label": "Название аккаунта",
"save": "Сохранить",
"failed": "Не удалось переименовать аккаунт. Попробуйте ещё раз."
}
}
},
+12 -2
View File
@@ -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<token>/getUpdates 中找到該聊天的 id。"
},
"rename": {
"title": "重新命名帳戶",
"description": "此名稱可讓你和 AI 將此帳戶與同一服務的其他帳戶區分開來。留空則顯示帳戶本身。",
"label": "帳戶名稱",
"save": "儲存",
"failed": "無法重新命名帳戶。請再試一次。"
}
}
},
+12 -2
View File
@@ -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<token>/getUpdates 中找到该聊天的 id。"
},
"rename": {
"title": "重命名账户",
"description": "此名称可让你和 AI 将此账户与同一服务的其他账户区分开。留空则显示账户本身。",
"label": "账户名称",
"save": "保存",
"failed": "无法重命名账户。请重试。"
}
}
},