mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-11 12:11:45 +00:00
Show each connected account as its own tool, managed in place
A connected tool's card has its switch again and names its account, so two accounts of one service are two tools you can turn on and off. Manage connection in its menu opens the connection panel on the Tools page instead of leaving for Connectors.
This commit is contained in:
1 parent
bacab4cfe7
commit
f4397a7a65
2 files changed
+95
-54
No files matched your search
@@ -102,6 +102,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 ? <div data-testid="drawer">{connector.key}</div> : null,
|
||||
}));
|
||||
vi.mock('../connectors/useConnectorLauncher', () => ({
|
||||
default: () => ({ launch: vi.fn(), modals: null }),
|
||||
}));
|
||||
|
||||
import connectorsReducer from '../connectors/connectorsSlice';
|
||||
import notificationsReducer from '../notifications/notificationsSlice';
|
||||
import Tools from './Tools';
|
||||
@@ -169,7 +179,12 @@ describe('Tools page', () => {
|
||||
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 () => {
|
||||
const card = (title: string) =>
|
||||
Array.from(container.querySelectorAll<HTMLElement>('h2'))
|
||||
.find((h) => h.textContent === title)!
|
||||
.closest<HTMLElement>('[data-slot="card"]')!;
|
||||
|
||||
it('opens the connection panel right here to manage a connected tool', async () => {
|
||||
await render();
|
||||
await openMenu(0);
|
||||
expect(menuItem('settings.tools.edit')).toBeUndefined();
|
||||
@@ -177,30 +192,26 @@ describe('Tools page', () => {
|
||||
menuItem('settings.connectors.manageConnection')!.click(),
|
||||
);
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="where"]')?.textContent,
|
||||
).toBe('/settings/connectors?connector=telegram');
|
||||
document.body.querySelector('[data-testid="drawer"]')?.textContent,
|
||||
).toBe('telegram');
|
||||
// Stays on the Tools page.
|
||||
expect(document.body.querySelector('[data-testid="where"]')).toBeNull();
|
||||
});
|
||||
|
||||
const card = (title: string) =>
|
||||
Array.from(container.querySelectorAll<HTMLElement>('h2'))
|
||||
.find((h) => h.textContent === title)!
|
||||
.closest<HTMLElement>('[data-slot="card"]')!;
|
||||
|
||||
it('manages a connected tool on its connector page, not with its own switch', async () => {
|
||||
it('shows each connected account as its own tool with its own switch', async () => {
|
||||
await render();
|
||||
const telegram = card('Telegram');
|
||||
expect(telegram.querySelector('[role="switch"]')).toBeNull();
|
||||
// The catalog's plain description, not the tool's setup notes.
|
||||
expect(telegram.querySelector('[role="switch"]')).not.toBeNull();
|
||||
// Which account this card is, and the catalog's plain description.
|
||||
expect(telegram.textContent).toContain('…abcd');
|
||||
expect(telegram.textContent).toContain(
|
||||
'settings.connectors.descriptions.telegram',
|
||||
);
|
||||
const manage = Array.from(telegram.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'settings.connectors.manageConnection',
|
||||
)!;
|
||||
await act(async () => manage.click());
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="where"]')?.textContent,
|
||||
).toBe('/settings/connectors?connector=telegram');
|
||||
Array.from(telegram.querySelectorAll('button')).some(
|
||||
(b) => b.textContent === 'settings.connectors.manageConnection',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('says a connected tool needs signing in again, with no raw MCP reconnect', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Switch } from '../components/ui/switch';
|
||||
import { ActionMenu, type MenuOption } from '../components/ui/dropdown-menu';
|
||||
import { EmptyState } from '../components/ui/empty-state';
|
||||
import ConnectionDrawer from '../connectors/ConnectionDrawer';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import {
|
||||
connectionNeedsSignIn,
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
selectConnectorCatalog,
|
||||
} from '../connectors/connectorsSlice';
|
||||
import { connectorDescription } from '../connectors/i18n';
|
||||
import type { Connection, ConnectorDefinition } from '../connectors/types';
|
||||
import useConnectorLauncher from '../connectors/useConnectorLauncher';
|
||||
import { useLoaderState } from '../hooks';
|
||||
import type { AvailableToolType } from '../modals/types';
|
||||
import AddToolModal from '../modals/AddToolModal';
|
||||
@@ -72,6 +75,10 @@ export default function Tools() {
|
||||
const [toolToShare, setToolToShare] = React.useState<UserToolType | null>(
|
||||
null,
|
||||
);
|
||||
// The connection panel, opened here for a connected tool's service.
|
||||
const [managed, setManaged] = React.useState<ConnectorDefinition | null>(
|
||||
null,
|
||||
);
|
||||
const [mcpStatuses, setMcpStatuses] = React.useState<{
|
||||
[toolId: string]: string;
|
||||
}>({});
|
||||
@@ -254,14 +261,29 @@ export default function Tools() {
|
||||
const handleSettingsClick = (tool: UserToolType) => {
|
||||
const connection = connectionOf(tool);
|
||||
if (connection) {
|
||||
navigate(
|
||||
`/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
|
||||
);
|
||||
const connector = catalog.find((c) => c.key === connection.connector_key);
|
||||
if (connector) setManaged(connector);
|
||||
else
|
||||
navigate(
|
||||
`/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSelectedTool(tool);
|
||||
};
|
||||
|
||||
const { launch, modals } = useConnectorLauncher({
|
||||
onConnected: () => getUserTools(),
|
||||
});
|
||||
// What tells two accounts of one service apart on their cards.
|
||||
const accountLine = (connection: Connection) =>
|
||||
connection.auth_kind === 'api_key'
|
||||
? t('settings.connectors.detail.keyEnding', {
|
||||
hint: connection.account_label,
|
||||
interpolation: { escapeValue: false },
|
||||
})
|
||||
: connection.account_label;
|
||||
|
||||
const handleGoBack = () => {
|
||||
setSelectedTool(null);
|
||||
getUserTools();
|
||||
@@ -447,15 +469,11 @@ export default function Tools() {
|
||||
{t('settings.tools.builtIn')}
|
||||
</Badge>
|
||||
)}
|
||||
{connectionNeedsSignIn(connection) ? (
|
||||
{connectionNeedsSignIn(connection) && (
|
||||
<Badge variant="warning">
|
||||
{t('settings.connectors.health.signInAgain')}
|
||||
</Badge>
|
||||
) : !tool.status ? (
|
||||
<Badge variant="neutral">
|
||||
{t('settings.tools.off')}
|
||||
</Badge>
|
||||
) : null}
|
||||
)}
|
||||
{tool.name === 'mcp_tool' &&
|
||||
!connection &&
|
||||
mcpStatuses[tool.id] && (
|
||||
@@ -509,37 +527,36 @@ export default function Tools() {
|
||||
</div>
|
||||
</div>
|
||||
{connection && (
|
||||
// The way to the connection's page, marked with
|
||||
// the service it comes from.
|
||||
// Which account this is: each account of a
|
||||
// service is its own tool. mr-12 keeps the line
|
||||
// clear of the switch.
|
||||
<CardFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
>
|
||||
<ConnectorIcon icon={connection.icon} />
|
||||
{t('settings.connectors.manageConnection')}
|
||||
</Button>
|
||||
<span className="mr-12 flex min-w-0 items-center gap-2">
|
||||
<ConnectorIcon
|
||||
icon={connection.icon}
|
||||
className="size-3.5 shrink-0"
|
||||
/>
|
||||
<span
|
||||
className="truncate"
|
||||
title={accountLine(connection)}
|
||||
>
|
||||
{accountLine(connection)}
|
||||
</span>
|
||||
</span>
|
||||
</CardFooter>
|
||||
)}
|
||||
{/* A connected tool is turned on and off on its
|
||||
connection's page, the one home for it. */}
|
||||
{!connection && (
|
||||
<div className="absolute right-4 bottom-4">
|
||||
<Switch
|
||||
checked={tool.status}
|
||||
onCheckedChange={(checked) =>
|
||||
updateToolStatus(tool.id, checked)
|
||||
}
|
||||
id={`toolToggle-${index}`}
|
||||
aria-label={t('settings.tools.toggleToolAria', {
|
||||
toolName: tool.customName || tool.displayName,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute right-4 bottom-4">
|
||||
<Switch
|
||||
checked={tool.status}
|
||||
onCheckedChange={(checked) =>
|
||||
updateToolStatus(tool.id, checked)
|
||||
}
|
||||
id={`toolToggle-${index}`}
|
||||
aria-label={t('settings.tools.toggleToolAria', {
|
||||
toolName: tool.customName || tool.displayName,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
@@ -577,6 +594,19 @@ export default function Tools() {
|
||||
fetchMcpStatuses();
|
||||
}}
|
||||
/>
|
||||
<ConnectionDrawer
|
||||
connector={managed}
|
||||
onClose={() => {
|
||||
setManaged(null);
|
||||
getUserTools();
|
||||
dispatch(loadConnectors({ token }));
|
||||
}}
|
||||
onConnect={(connector, options) => {
|
||||
setManaged(null);
|
||||
launch(connector, options);
|
||||
}}
|
||||
/>
|
||||
{modals}
|
||||
{toolToShare && (
|
||||
<ShareToTeamModal
|
||||
resourceType="tool"
|
||||
|
||||
Reference in new issue
Block a user