A connector's page is the one home for its account, knowledge and tools

- Tools from a connection turn on and off on the connector's page (a
  real switch, not a badge), next to their action permissions.
- Knowledge synced from a connection lists its sync state there, with
  Sync now (connector sources and S3/Reddit sources each through their
  own endpoint).
- On the Tools and Knowledge pages, anything from a connection has
  "Manage connection" in its menu, which opens that page; the tool
  editor with its second credentials form is no longer offered for them.
- A link to a part (Jira & Confluence) opens its parent's page.
This commit is contained in:
arc53-machine committed 2026-09-28 22:38:49 +01:00
1 parent 3b1288097d
commit 79fd2ac6cd
14 files changed
+559 -31

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@ Open **Settings > Connectors**. Filters narrow the list by category, and **Conne
**Knowledge** (formerly Sources) is what the assistant searches: uploads, links, and content synced from your connections. The **Add knowledge** and **Add Tool** dialogs, the composer's Knowledge and Tools menus, and the agent builder all start the same flow, and list tools grouped by the connection they use. A service offered two ways appears as one card: Confluence syncs pages into Knowledge and, through **Jira & Confluence**, lets agents search and update Jira and Confluence.
Click a connected card to see its account, the sources and tools that use it, and to **Reconnect** or **Disconnect**. Disconnecting keeps a source's indexed content but stops its sync.
A connector's page is the one place to manage it: its accounts (**Reconnect**, **Disconnect**), the Knowledge it syncs (**Sync now**), and its tools (on or off, and what each action may do). On the Knowledge and Tools pages, anything from a connection has **Manage connection** in its menu, which opens that page. Disconnecting keeps synced content but stops its sync.
### When a connection needs attention
@@ -0,0 +1,198 @@
import { configureStore } from '@reduxjs/toolkit';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { Provider } from 'react-redux';
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
const connectors = vi.hoisted(() => ({
getConnection: vi.fn(),
getCatalog: vi.fn(async () => ({ success: true, connectors: [] })),
listConnections: vi.fn(async () => ({ success: true, connections: [] })),
}));
vi.mock('../api/services/connectorsService', () => ({ default: connectors }));
const users = vi.hoisted(() => ({
updateToolStatus: vi.fn(),
syncConnector: vi.fn(),
syncSource: vi.fn(),
}));
vi.mock('../api/services/userService', () => ({ default: users }));
import actionToastReducer, {
selectActionToast,
} from '../notifications/actionToastSlice';
import ConnectionDrawer from './ConnectionDrawer';
import connectorsReducer from './connectorsSlice';
import type { ConnectorDefinition } from './types';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
const DRIVE = {
key: 'google_drive',
name: 'Google Drive',
description: 'Sync files.',
icon: 'drive',
publisher: 'built_in',
auth_kind: 'oauth',
capabilities: ['sync'],
setup: { tools: 'off', sync: 'ask' },
sync_ingestor: 'google_drive',
available: true,
needs_setup: false,
missing_settings: [],
} as unknown as ConnectorDefinition;
const DETAIL = {
id: 'conn-1',
connector_key: 'google_drive',
auth_kind: 'oauth',
account_label: 'lena@example.com',
status: 'connected',
last_error: null,
server_url: null,
sources: [
{
id: 'src-1',
name: 'Handbook',
type: 'connector:file',
last_sync: null,
sync_frequency: 'weekly',
sync_state: 'active',
},
],
tools: [
{
id: 'tool-1',
name: 'mcp_tool',
display_name: 'Drive search',
status: true,
credential_mode: 'owner',
actions: [],
},
],
};
describe('ConnectionDrawer', () => {
let container: HTMLDivElement;
let root: Root;
let store: ReturnType<typeof makeStore>;
const makeStore = () =>
configureStore({
reducer: {
connectors: connectorsReducer,
actionToast: actionToastReducer,
preference: (state = { token: null }) => state,
},
preloadedState: {
connectors: {
enabled: true,
loading: false,
loaded: true,
failed: false,
catalog: [DRIVE],
connections: [
{
id: 'conn-1',
connector_key: 'google_drive',
status: 'connected',
},
],
},
preference: { token: null },
},
} as Parameters<typeof configureStore>[0]);
beforeEach(() => {
Object.values(users).forEach((fn) => fn.mockReset());
connectors.getConnection.mockResolvedValue({
success: true,
connection: DETAIL,
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
});
const render = async () => {
store = makeStore();
await act(async () => {
root.render(
<Provider store={store}>
<ConnectionDrawer
connector={DRIVE}
onClose={vi.fn()}
onConnect={vi.fn()}
/>
</Provider>,
);
});
};
const button = (text: string) =>
Array.from(
document.body.querySelectorAll<HTMLButtonElement>('button'),
).find((b) => b.textContent === text)!;
it('turns a tool on and off from its connection page', async () => {
users.updateToolStatus.mockResolvedValue({ ok: true });
await render();
const toggle = document.body.querySelector<HTMLButtonElement>(
'[aria-label="settings.connectors.detail.toolSwitch"]',
)!;
expect(toggle.getAttribute('aria-checked')).toBe('true');
await act(async () => toggle.click());
expect(users.updateToolStatus).toHaveBeenCalledWith(
{ id: 'tool-1', status: false },
null,
);
expect(toggle.getAttribute('aria-checked')).toBe('false');
});
it('puts the switch back when the change fails', async () => {
users.updateToolStatus.mockResolvedValue({ ok: false });
await render();
const toggle = document.body.querySelector<HTMLButtonElement>(
'[aria-label="settings.connectors.detail.toolSwitch"]',
)!;
await act(async () => toggle.click());
expect(toggle.getAttribute('aria-checked')).toBe('true');
expect(
selectActionToast(
store.getState() as Parameters<typeof selectActionToast>[0],
)?.variant,
).toBe('destructive');
});
it('syncs a source now through its connection', async () => {
users.syncConnector.mockResolvedValue({
json: async () => ({ success: true }),
});
await render();
await act(async () => button('settings.connectors.detail.syncNow').click());
expect(users.syncConnector).toHaveBeenCalledWith('src-1', null);
expect(users.syncSource).not.toHaveBeenCalled();
expect(
selectActionToast(
store.getState() as Parameters<typeof selectActionToast>[0],
)?.variant,
).toBe('success');
});
it('labels synced content as Knowledge from this connection', async () => {
await render();
expect(document.body.textContent).toContain(
'settings.connectors.detail.sources',
);
expect(document.body.textContent).not.toContain(
'settings.connectors.publisher',
);
});
});
+99 -8
View File
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import connectorsService from '../api/services/connectorsService';
import userService from '../api/services/userService';
import { Alert, AlertDescription } from '../components/ui/alert';
import { Badge } from '../components/ui/badge';
import { Button } from '../components/ui/button';
@@ -23,6 +24,7 @@ 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 { Switch } from '../components/ui/switch';
import {
Sheet,
SheetContent,
@@ -43,6 +45,8 @@ import { connectorDescription, connectorName } from './i18n';
import ToolPermissions from './ToolPermissions';
import type {
ConnectionDetail,
ConnectionSource,
ConnectionTool,
ConnectionStatus,
ConnectorDefinition,
} from './types';
@@ -187,6 +191,33 @@ function RemoveConnectionModal({
);
}
/** A connection tool's on/off switch: the one place it is turned on or off. */
function ToolSwitch({
tool,
onToggle,
}: {
tool: ConnectionTool;
onToggle: (toolId: string, on: boolean) => Promise<boolean>;
}) {
const { t } = useTranslation();
const [on, setOn] = useState(tool.status);
useEffect(() => setOn(tool.status), [tool.status]);
return (
<Switch
checked={on}
aria-label={t('settings.connectors.detail.toolSwitch', {
name: tool.display_name,
interpolation: { escapeValue: false },
})}
onCheckedChange={async (checked) => {
const next = checked === true;
setOn(next);
if (!(await onToggle(tool.id, next))) setOn(!next);
}}
/>
);
}
function AccountSection({
connector,
detail,
@@ -195,6 +226,8 @@ function AccountSection({
onRemove,
onSyncMore,
onRefreshTools,
onToggleTool,
onSyncNow,
}: {
connector: ConnectorDefinition;
detail: ConnectionDetail;
@@ -203,6 +236,9 @@ function AccountSection({
onRemove: (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. */
onToggleTool: (toolId: string, on: boolean) => Promise<boolean>;
onSyncNow: (source: ConnectionSource) => void;
}) {
const { t } = useTranslation();
const accountTitle = useAccountTitle();
@@ -313,7 +349,17 @@ function AccountSection({
<Badge variant="warning">
{t('settings.connectors.detail.paused')}
</Badge>
) : undefined
) : (
<Button
type="button"
variant="outline"
size="sm"
shape="pill"
onClick={() => onSyncNow(source)}
>
{t('settings.connectors.detail.syncNow')}
</Button>
)
}
/>
))}
@@ -353,13 +399,7 @@ function AccountSection({
as="h4"
size="xs"
title={tool.display_name}
actions={
<Badge variant={tool.status ? 'success' : 'neutral'}>
{tool.status
? t('settings.connectors.detail.toolOn')
: t('settings.connectors.detail.toolOff')}
</Badge>
}
actions={<ToolSwitch tool={tool} onToggle={onToggleTool} />}
/>
<ToolPermissions
connectionId={detail.id}
@@ -482,6 +522,53 @@ export default function ConnectionDrawer({
});
};
const toggleTool = async (toolId: string, on: boolean) => {
try {
const response = await userService.updateToolStatus(
{ id: toolId, status: on },
token,
);
if (!response.ok) throw new Error('toggle failed');
return true;
} catch {
dispatch(
showActionToast({
variant: 'destructive',
message: t('settings.connectors.detail.toolSwitchFailed'),
}),
);
return false;
}
};
const syncNow = async (source: ConnectionSource) => {
try {
// Drive, SharePoint and Confluence sources sync through the connector
// endpoint; S3 and Reddit through the remote-source one.
const response = source.type?.startsWith('connector')
? await userService.syncConnector(source.id, token)
: await userService.syncSource({ source_id: source.id }, token);
const data = await response.json();
if (!data?.success) throw new Error('sync failed');
dispatch(
showActionToast({
variant: 'success',
message: t('settings.connectors.detail.syncStarted', {
name: source.name,
interpolation: { escapeValue: false },
}),
}),
);
} catch {
dispatch(
showActionToast({
variant: 'destructive',
message: t('settings.connectors.detail.syncFailed'),
}),
);
}
};
const refreshTools = async (detail: ConnectionDetail) => {
const data = await connectorsService.refreshTools(detail.id, token);
dispatch(
@@ -617,6 +704,8 @@ export default function ConnectionDrawer({
})
}
onRefreshTools={refreshTools}
onToggleTool={toggleTool}
onSyncNow={syncNow}
/>
))}
</div>
@@ -660,6 +749,8 @@ export default function ConnectionDrawer({
onConnect(part, { mode: 'sync', connectionId: d.id })
}
onRefreshTools={refreshTools}
onToggleTool={toggleTool}
onSyncNow={syncNow}
/>
))}
</section>
+8 -2
View File
@@ -1184,7 +1184,12 @@
"noSources": "Aus dieser Verbindung wurde noch nichts synchronisiert.",
"remove": "Entfernen",
"refreshed": "Werkzeuge aktualisiert: {{added}} hinzugefügt, {{removed}} entfernt.",
"refreshFailed": "Die Werkzeuge konnten nicht aktualisiert werden."
"refreshFailed": "Die Werkzeuge konnten nicht aktualisiert werden.",
"syncNow": "Jetzt synchronisieren",
"syncStarted": "{{name}} wird synchronisiert…",
"syncFailed": "Die Synchronisierung konnte nicht gestartet werden. Versuche es erneut.",
"toolSwitch": "{{name}} an",
"toolSwitchFailed": "Das Werkzeug konnte nicht geändert werden. Versuche es erneut."
},
"permission": {
"always": "Immer erlauben",
@@ -1292,7 +1297,8 @@
"sync": "Synchronisiert ins Wissen",
"read": "Schlägt nach",
"write": "Führt Aktionen aus"
}
},
"manageConnection": "Verbindung verwalten"
}
},
"modals": {
+8 -2
View File
@@ -1190,7 +1190,12 @@
"noSources": "Nothing synced from this connection yet.",
"remove": "Remove",
"refreshed": "Tools refreshed: {{added}} added, {{removed}} removed.",
"refreshFailed": "Could not refresh the tools."
"refreshFailed": "Could not refresh the tools.",
"syncNow": "Sync now",
"syncStarted": "Syncing {{name}}…",
"syncFailed": "Could not start the sync. Try again.",
"toolSwitch": "{{name}} on",
"toolSwitchFailed": "Could not change the tool. Try again."
},
"permission": {
"always": "Always allow",
@@ -1298,7 +1303,8 @@
"sync": "Syncs into Knowledge",
"read": "Looks things up",
"write": "Takes actions"
}
},
"manageConnection": "Manage connection"
}
},
"modals": {
+8 -2
View File
@@ -1184,7 +1184,12 @@
"noSources": "Aún no se ha sincronizado nada desde esta conexión.",
"remove": "Eliminar",
"refreshed": "Herramientas actualizadas: {{added}} añadidas, {{removed}} eliminadas.",
"refreshFailed": "No se pudieron actualizar las herramientas."
"refreshFailed": "No se pudieron actualizar las herramientas.",
"syncNow": "Sincronizar ahora",
"syncStarted": "Sincronizando {{name}}…",
"syncFailed": "No se pudo iniciar la sincronización. Inténtalo de nuevo.",
"toolSwitch": "{{name}} activado",
"toolSwitchFailed": "No se pudo cambiar la herramienta. Inténtalo de nuevo."
},
"permission": {
"always": "Permitir siempre",
@@ -1292,7 +1297,8 @@
"sync": "Sincroniza en Conocimiento",
"read": "Consulta datos",
"write": "Realiza acciones"
}
},
"manageConnection": "Gestionar conexión"
}
},
"modals": {
+8 -2
View File
@@ -1175,7 +1175,12 @@
"noSources": "この接続からはまだ何も同期されていません。",
"remove": "削除",
"refreshed": "ツールを更新しました: {{added}} 件追加、{{removed}} 件削除。",
"refreshFailed": "ツールを更新できませんでした。"
"refreshFailed": "ツールを更新できませんでした。",
"syncNow": "今すぐ同期",
"syncStarted": "{{name}} を同期しています…",
"syncFailed": "同期を開始できませんでした。もう一度お試しください。",
"toolSwitch": "{{name}} をオン",
"toolSwitchFailed": "ツールを変更できませんでした。もう一度お試しください。"
},
"permission": {
"always": "常に許可",
@@ -1276,7 +1281,8 @@
"sync": "ナレッジに同期",
"read": "情報を調べる",
"write": "操作を行う"
}
},
"manageConnection": "接続を管理"
}
},
"modals": {
+8 -2
View File
@@ -1240,7 +1240,12 @@
"noSources": "Из этого подключения пока ничего не синхронизировано.",
"remove": "Удалить",
"refreshed": "Инструменты обновлены: добавлено {{added}}, удалено {{removed}}.",
"refreshFailed": "Не удалось обновить инструменты."
"refreshFailed": "Не удалось обновить инструменты.",
"syncNow": "Синхронизировать",
"syncStarted": "Синхронизация {{name}}…",
"syncFailed": "Не удалось запустить синхронизацию. Попробуйте ещё раз.",
"toolSwitch": "{{name}} включён",
"toolSwitchFailed": "Не удалось изменить инструмент. Попробуйте ещё раз."
},
"permission": {
"always": "Всегда разрешать",
@@ -1362,7 +1367,8 @@
"sync": "Синхронизирует в знания",
"read": "Ищет информацию",
"write": "Выполняет действия"
}
},
"manageConnection": "Управлять подключением"
}
},
"modals": {
+8 -2
View File
@@ -1175,7 +1175,12 @@
"noSources": "尚未從此連線同步任何內容。",
"remove": "移除",
"refreshed": "工具已重新整理:新增 {{added}} 個,移除 {{removed}} 個。",
"refreshFailed": "無法重新整理工具。"
"refreshFailed": "無法重新整理工具。",
"syncNow": "立即同步",
"syncStarted": "正在同步 {{name}}…",
"syncFailed": "無法開始同步。請再試一次。",
"toolSwitch": "{{name}} 開啟",
"toolSwitchFailed": "無法變更工具。請再試一次。"
},
"permission": {
"always": "一律允許",
@@ -1276,7 +1281,8 @@
"sync": "同步到知識庫",
"read": "查詢資訊",
"write": "執行操作"
}
},
"manageConnection": "管理連線"
}
},
"modals": {
+8 -2
View File
@@ -1175,7 +1175,12 @@
"noSources": "尚未从此连接同步任何内容。",
"remove": "移除",
"refreshed": "工具已刷新:新增 {{added}} 个,移除 {{removed}} 个。",
"refreshFailed": "无法刷新工具。"
"refreshFailed": "无法刷新工具。",
"syncNow": "立即同步",
"syncStarted": "正在同步 {{name}}…",
"syncFailed": "无法开始同步。请重试。",
"toolSwitch": "{{name}} 开启",
"toolSwitchFailed": "无法更改工具。请重试。"
},
"permission": {
"always": "始终允许",
@@ -1276,7 +1281,8 @@
"sync": "同步到知识库",
"read": "查找信息",
"write": "执行操作"
}
},
"manageConnection": "管理连接"
}
},
"modals": {
+7 -1
View File
@@ -88,7 +88,13 @@ export default function Connectors() {
}, [dispatch, token]);
const custom = catalog.filter((c) => c.publisher === 'custom');
const openConnector = catalog.find((c) => c.key === openKey) ?? null;
// A link to a part (a tool's "Manage connection") opens its parent's page.
const openTarget = catalog.find((c) => c.key === openKey);
const openConnector =
(openTarget?.part_of &&
catalog.find((c) => c.key === openTarget.part_of)) ||
openTarget ||
null;
// One service offered two ways (Confluence sync and the Jira & Confluence
// MCP actions) is one card. A part is shown on its own only when its
+20
View File
@@ -5,6 +5,7 @@ import {
Eye,
HardDrive,
Network,
Plug,
RefreshCw,
Search,
SlidersHorizontal,
@@ -14,6 +15,7 @@ import {
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import userService from '../api/services/userService';
import modelService from '../api/services/modelService';
@@ -90,6 +92,7 @@ export default function Sources({
handleDeleteDocument,
}: DocumentsProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch<AppDispatch>();
const token = useSelector(selectToken);
const uploadTasks = useSelector(selectUploadTasks);
@@ -341,6 +344,23 @@ export default function Sources({
},
];
// Synced from a connection: its account, sync and other uses live on
// the connector's page.
const connection = document.connectionId
? connections.find((c) => c.id === document.connectionId)
: undefined;
if (connection) {
actions.push({
icon: Plug,
label: t('settings.connectors.manageConnection'),
onClick: () =>
navigate(
`/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
),
variant: 'default',
});
}
if (document.ingestStatus === 'failed') {
actions.push({
icon: RefreshCw,
+151
View File
@@ -0,0 +1,151 @@
import { configureStore } from '@reduxjs/toolkit';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { Provider } from 'react-redux';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
const TOOLS = [
{
id: 'tg',
name: 'telegram',
displayName: 'Telegram',
customName: '',
description: 'Send messages',
status: true,
config: {},
actions: [],
connection_id: 'conn-1',
},
{
id: 'api',
name: 'api_tool',
displayName: 'API Tool',
customName: 'My API',
description: 'Calls an API',
status: true,
config: {},
actions: [],
connection_id: null,
},
];
vi.mock('../api/services/userService', () => ({
default: {
getUserTools: vi.fn(async () => ({ json: async () => ({ tools: TOOLS }) })),
getMCPAuthStatus: vi.fn(async () => ({
json: async () => ({ statuses: {} }),
})),
getAvailableTools: vi.fn(async () => ({
json: async () => ({ data: [] }),
})),
},
}));
vi.mock('../api/services/connectorsService', () => ({
default: {
getCatalog: vi.fn(async () => ({ success: true, connectors: [] })),
listConnections: vi.fn(async () => ({
success: true,
connections: [
{
id: 'conn-1',
connector_key: 'telegram',
name: 'Telegram',
icon: 'tool_telegram',
status: 'connected',
account_label: '…abcd',
},
],
})),
},
}));
import connectorsReducer from '../connectors/connectorsSlice';
import notificationsReducer from '../notifications/notificationsSlice';
import Tools from './Tools';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
function Where() {
const location = useLocation();
return <div data-testid="where">{location.pathname + location.search}</div>;
}
describe('Tools page', () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
});
const render = async () => {
const store = configureStore({
reducer: {
connectors: connectorsReducer,
notifications: notificationsReducer,
preference: (state = { token: null }) => state,
},
});
await act(async () => {
root.render(
<Provider store={store}>
<MemoryRouter initialEntries={['/settings/tools']}>
<Routes>
<Route path="/settings/tools" element={<Tools />} />
<Route path="/settings/connectors" element={<Where />} />
</Routes>
</MemoryRouter>
</Provider>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
});
};
const openMenu = async (index: number) => {
const triggers = container.querySelectorAll<HTMLButtonElement>(
'[aria-label="settings.tools.settingsIconAlt"]',
);
await act(async () => {
triggers[index].dispatchEvent(
new PointerEvent('pointerdown', { bubbles: true, button: 0 }),
);
});
};
const menuItem = (text: string) =>
Array.from(
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]'),
).find((item) => item.textContent?.includes(text));
it('sends a connected tool to its connector page to manage it', async () => {
await render();
await openMenu(0);
expect(menuItem('settings.tools.edit')).toBeUndefined();
await act(async () =>
menuItem('settings.connectors.manageConnection')!.click(),
);
expect(
document.body.querySelector('[data-testid="where"]')?.textContent,
).toBe('/settings/connectors?connector=telegram');
});
it('keeps Edit for a tool that is not from a connection', async () => {
await render();
await openMenu(1);
expect(menuItem('settings.tools.edit')).toBeDefined();
expect(menuItem('settings.connectors.manageConnection')).toBeUndefined();
});
});
+27 -7
View File
@@ -1,4 +1,4 @@
import { Pencil, RefreshCw, Trash2, Users } from 'lucide-react';
import { Pencil, Plug, RefreshCw, Trash2, Users } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -148,12 +148,19 @@ export default function Tools() {
const getMenuOptions = (tool: UserToolType): MenuOption[] => {
const options: MenuOption[] = [
{
icon: Pencil,
label: t('settings.tools.edit'),
onClick: () => handleSettingsClick(tool),
variant: 'default',
},
connectionOf(tool)
? {
icon: Plug,
label: t('settings.connectors.manageConnection'),
onClick: () => handleSettingsClick(tool),
variant: 'default',
}
: {
icon: Pencil,
label: t('settings.tools.edit'),
onClick: () => handleSettingsClick(tool),
variant: 'default',
},
{
icon: Trash2,
label: t('settings.tools.delete'),
@@ -234,7 +241,20 @@ export default function Tools() {
});
};
// A connected tool is managed on its connector's page (account, on/off,
// permissions); only other tools open the tool editor here.
const connectionOf = (tool: UserToolType) =>
tool.connection_id
? connections.find((c) => c.id === tool.connection_id)
: undefined;
const handleSettingsClick = (tool: UserToolType) => {
const connection = connectionOf(tool);
if (connection) {
navigate(
`/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
);
return;
}
setSelectedTool(tool);
};