mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 02:14:23 +00:00
Simplify Admin > Connectors
- One control for members' own MCP servers: the custom MCP server row's
switch (it sets both the policy and the instance switch); the separate
top-level switch is gone.
- Connectors that only sync content show "No tools" instead of a
sharing policy, and the policies read "The sharer decides per share",
"Always the sharer's account", "Always each person's own account".
- A connector that needs server settings can't be switched on until they
exist; a tooltip says why.
- On phones the table becomes a list ("On · 2 connections · …") and each
connector's controls open in a sheet.
This commit is contained in:
1 parent
23229181f1
commit
df70869dc7
5 files changed
+304
-110
No files matched your search
@@ -50,11 +50,11 @@ Tool calls name the service that ran them: "Searched Notion", "Read from Google
|
||||
Admins manage connectors in **Admin > Connectors**. It lists every connector with its status, how many connections use it, and:
|
||||
|
||||
- **Enabled** turns a connector off for everyone. Members no longer see it (except to manage a connection they already have), its tools stop working, and its sources stop syncing until you turn it back on. Existing connections are kept.
|
||||
- **Shared tools use** is **Let owners choose** (default), **Always the owner's account** or **Always each member's own account**.
|
||||
- **Allow custom MCP servers** controls whether members can add their own MCP servers. Presets are enabled one by one.
|
||||
- **Shared tools use** is **The sharer decides per share** (default), **Always the sharer's account** or **Always each person's own account**. Connectors that only sync content have no tools and show **No tools**.
|
||||
- The **MCP server** row decides whether members can add their own MCP servers. Presets are switched one by one.
|
||||
- **Setup guide** on an OAuth connector shows the redirect URI to register and which server settings are still missing.
|
||||
|
||||
A connector that still needs server settings starts turned off and is hidden from members. It turns on once its settings are present, unless you switched it off; switching it on before then has no effect for members.
|
||||
A connector that still needs server settings starts turned off and is hidden from members; its switch stays disabled until the settings are present. It turns on once they are, unless you switched it off. On a phone, the page lists the connectors and opens each one's controls in a panel.
|
||||
|
||||
### Encryption key
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class AdminConnectorsResource(Resource):
|
||||
"category": definition.category,
|
||||
"publisher": definition.publisher,
|
||||
"auth_kind": definition.auth_kind,
|
||||
"capabilities": list(definition.capabilities),
|
||||
"enabled": service.connector_is_enabled(policies, definition.key),
|
||||
"credential_mode": policy.get("credential_mode", "choose"),
|
||||
"configured": definition.configured,
|
||||
|
||||
@@ -30,6 +30,7 @@ const connector = (overrides: Record<string, unknown> = {}) => ({
|
||||
icon: 'google-drive',
|
||||
publisher: 'built_in',
|
||||
auth_kind: 'oauth',
|
||||
capabilities: ['sync'],
|
||||
enabled: true,
|
||||
credential_mode: 'choose',
|
||||
configured: false,
|
||||
@@ -43,6 +44,29 @@ const connector = (overrides: Record<string, unknown> = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const MCP_ROW = connector({
|
||||
key: 'custom_mcp',
|
||||
name: 'MCP server',
|
||||
icon: 'mcp',
|
||||
publisher: 'custom',
|
||||
auth_kind: 'mcp',
|
||||
capabilities: ['read', 'write'],
|
||||
configured: true,
|
||||
required_settings: [],
|
||||
connection_count: 0,
|
||||
});
|
||||
const NOTION = {
|
||||
key: 'mcp_notion',
|
||||
name: 'Notion',
|
||||
icon: 'notion',
|
||||
publisher: 'preset',
|
||||
auth_kind: 'mcp_oauth',
|
||||
capabilities: ['read', 'write'],
|
||||
configured: true,
|
||||
required_settings: [],
|
||||
connection_count: 0,
|
||||
};
|
||||
|
||||
const payload = (overrides: Record<string, unknown> = {}) => ({
|
||||
success: true,
|
||||
connectors: [
|
||||
@@ -53,10 +77,12 @@ const payload = (overrides: Record<string, unknown> = {}) => ({
|
||||
icon: 'notion',
|
||||
publisher: 'preset',
|
||||
auth_kind: 'mcp_oauth',
|
||||
capabilities: ['read', 'write'],
|
||||
configured: true,
|
||||
required_settings: [],
|
||||
connection_count: 0,
|
||||
}),
|
||||
MCP_ROW,
|
||||
],
|
||||
allow_custom_mcp: true,
|
||||
default_encryption_key: false,
|
||||
@@ -106,7 +132,7 @@ describe('Admin Connectors', () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
await render();
|
||||
const rows = Array.from(container.querySelectorAll('tbody tr'));
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0].textContent).toContain('Google Drive');
|
||||
expect(rows[0].textContent).toContain('Needs setup');
|
||||
expect(rows[0].textContent).toContain('Setup guide');
|
||||
@@ -130,24 +156,74 @@ describe('Admin Connectors', () => {
|
||||
it('saves a connector toggle as a policy', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
updateAdmin.mockResolvedValue(
|
||||
payload({
|
||||
connectors: [connector({ enabled: false })],
|
||||
}),
|
||||
payload({ connectors: [connector({ ...NOTION, enabled: false })] }),
|
||||
);
|
||||
await render();
|
||||
const toggle = container.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Google Drive enabled"]',
|
||||
)!;
|
||||
await act(async () => toggle.click());
|
||||
const toggle = () =>
|
||||
container.querySelector<HTMLButtonElement>(
|
||||
'table [aria-label="Notion enabled"]',
|
||||
)!;
|
||||
await act(async () => toggle().click());
|
||||
expect(updateAdmin).toHaveBeenCalledWith(
|
||||
{ policies: { google_drive: { enabled: false } } },
|
||||
{ policies: { mcp_notion: { enabled: false } } },
|
||||
null,
|
||||
);
|
||||
expect(toggle().getAttribute('aria-checked')).toBe('false');
|
||||
});
|
||||
|
||||
it('keeps a connector that needs setup off, and says why', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
await render();
|
||||
const drive = container.querySelector<HTMLButtonElement>(
|
||||
'table [aria-label="Google Drive enabled"]',
|
||||
)!;
|
||||
expect(drive.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('shows no sharing policy for a sync-only connector', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
await render();
|
||||
const [drive, notion] = Array.from(container.querySelectorAll('tbody tr'));
|
||||
expect(drive.textContent).toContain('No tools');
|
||||
expect(
|
||||
container
|
||||
.querySelector('[aria-label="Google Drive enabled"]')
|
||||
?.getAttribute('aria-checked'),
|
||||
).toBe('false');
|
||||
notion.querySelector('[aria-label="Notion sharing policy"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('turns custom MCP servers off from their own row', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
updateAdmin.mockResolvedValue(payload({ allow_custom_mcp: false }));
|
||||
await render();
|
||||
const mcp = () =>
|
||||
container.querySelector<HTMLButtonElement>(
|
||||
'table [aria-label="MCP server enabled"]',
|
||||
)!;
|
||||
await act(async () => mcp().click());
|
||||
expect(updateAdmin).toHaveBeenCalledWith(
|
||||
{ allow_custom_mcp: false, policies: { custom_mcp: { enabled: false } } },
|
||||
null,
|
||||
);
|
||||
expect(mcp().getAttribute('aria-checked')).toBe('false');
|
||||
expect(container.querySelector('#allow-custom-mcp')).toBeNull();
|
||||
});
|
||||
|
||||
it('lists connectors on phones and opens their controls in a sheet', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
await render();
|
||||
const rows = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(
|
||||
'[data-slot="list-row"] button',
|
||||
),
|
||||
);
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0].textContent).toContain(
|
||||
'Needs setup · 3 connections · No tools',
|
||||
);
|
||||
await act(async () => rows[1].click());
|
||||
expect(
|
||||
document.body.querySelector('[data-slot="sheet-content"]'),
|
||||
).not.toBeNull();
|
||||
expect(document.body.textContent).toContain('Shared tools use');
|
||||
});
|
||||
|
||||
it('reports a failed save in a toast and keeps the page', async () => {
|
||||
@@ -155,14 +231,21 @@ describe('Admin Connectors', () => {
|
||||
updateAdmin.mockResolvedValue({ success: false });
|
||||
await render();
|
||||
await act(async () =>
|
||||
container.querySelector<HTMLButtonElement>('#allow-custom-mcp')!.click(),
|
||||
container
|
||||
.querySelector<HTMLButtonElement>(
|
||||
'table [aria-label="Notion enabled"]',
|
||||
)!
|
||||
.click(),
|
||||
);
|
||||
expect(updateAdmin).toHaveBeenCalledWith(
|
||||
{ policies: { mcp_notion: { enabled: false } } },
|
||||
null,
|
||||
);
|
||||
expect(updateAdmin).toHaveBeenCalledWith({ allow_custom_mcp: false }, null);
|
||||
expect(selectActionToast(store.getState())).toMatchObject({
|
||||
variant: 'destructive',
|
||||
message: 'Could not save the change.',
|
||||
});
|
||||
expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
|
||||
expect(container.querySelectorAll('tbody tr')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('saves one change at a time so a late response cannot win', async () => {
|
||||
@@ -172,34 +255,28 @@ describe('Admin Connectors', () => {
|
||||
() => new Promise((resolve) => pending.push(resolve)),
|
||||
);
|
||||
await render();
|
||||
const toggle = () =>
|
||||
const notion = () =>
|
||||
container.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Google Drive enabled"]',
|
||||
'table [aria-label="Notion enabled"]',
|
||||
)!;
|
||||
await act(async () => toggle().click());
|
||||
await act(async () =>
|
||||
container.querySelector<HTMLButtonElement>('#allow-custom-mcp')!.click(),
|
||||
);
|
||||
const mcp = () =>
|
||||
container.querySelector<HTMLButtonElement>(
|
||||
'table [aria-label="MCP server enabled"]',
|
||||
)!;
|
||||
await act(async () => notion().click());
|
||||
await act(async () => mcp().click());
|
||||
// The second save waits for the first.
|
||||
expect(updateAdmin).toHaveBeenCalledTimes(1);
|
||||
await act(async () =>
|
||||
pending[0](payload({ connectors: [connector({ enabled: false })] })),
|
||||
);
|
||||
const off = connector({ ...NOTION, enabled: false });
|
||||
await act(async () => pending[0](payload({ connectors: [off, MCP_ROW] })));
|
||||
expect(updateAdmin).toHaveBeenCalledTimes(2);
|
||||
await act(async () =>
|
||||
pending[1](
|
||||
payload({
|
||||
connectors: [connector({ enabled: false })],
|
||||
allow_custom_mcp: false,
|
||||
}),
|
||||
payload({ connectors: [off, MCP_ROW], allow_custom_mcp: false }),
|
||||
),
|
||||
);
|
||||
expect(toggle().getAttribute('aria-checked')).toBe('false');
|
||||
expect(
|
||||
container
|
||||
.querySelector('#allow-custom-mcp')!
|
||||
.getAttribute('aria-checked'),
|
||||
).toBe('false');
|
||||
expect(notion().getAttribute('aria-checked')).toBe('false');
|
||||
expect(mcp().getAttribute('aria-checked')).toBe('false');
|
||||
});
|
||||
|
||||
it('offers a retry when loading fails', async () => {
|
||||
|
||||
@@ -9,6 +9,14 @@ import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card } from '../components/ui/card';
|
||||
import { FormField } from '../components/ui/form-field';
|
||||
import { ListRow, ListRows } from '../components/ui/list-row';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetTitle,
|
||||
} from '../components/ui/sheet';
|
||||
import {
|
||||
DescriptionItem,
|
||||
DescriptionList,
|
||||
@@ -34,6 +42,11 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../components/ui/table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '../components/ui/tooltip';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import { showActionToast } from '../notifications/actionToastSlice';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
@@ -47,6 +60,7 @@ type AdminConnector = {
|
||||
icon: string;
|
||||
publisher: 'built_in' | 'preset' | 'custom';
|
||||
auth_kind: string;
|
||||
capabilities: string[];
|
||||
enabled: boolean;
|
||||
credential_mode: Policy;
|
||||
configured: boolean;
|
||||
@@ -66,11 +80,14 @@ type AdminConnectorsData = {
|
||||
};
|
||||
|
||||
const POLICY_LABELS: Record<Policy, string> = {
|
||||
choose: 'Let owners choose',
|
||||
member: "Always each member's own account",
|
||||
owner: "Always the owner's account",
|
||||
choose: 'The sharer decides per share',
|
||||
member: "Always each person's own account",
|
||||
owner: "Always the sharer's account",
|
||||
};
|
||||
|
||||
const hasTools = (connector: AdminConnector) =>
|
||||
connector.capabilities.some((capability) => capability !== 'sync');
|
||||
|
||||
function CodeRow({ value }: { value: string }) {
|
||||
return (
|
||||
<Card variant="filled" padding="sm" className="flex-row items-start gap-2">
|
||||
@@ -161,6 +178,7 @@ export default function Connectors() {
|
||||
const [data, setData] = useState<AdminConnectorsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [guide, setGuide] = useState<AdminConnector | null>(null);
|
||||
const [detailKey, setDetailKey] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -207,6 +225,94 @@ export default function Connectors() {
|
||||
if (!data?.success)
|
||||
return <LoadError message="Failed to load connectors." onRetry={load} />;
|
||||
|
||||
// The sheet (phones) always shows the connector's latest saved state.
|
||||
const detail = data.connectors.find((c) => c.key === detailKey) ?? null;
|
||||
|
||||
// The custom MCP row is the one switch for members' own MCP servers.
|
||||
const isCustomMcp = (connector: AdminConnector) =>
|
||||
connector.key === 'custom_mcp';
|
||||
const enabledOf = (connector: AdminConnector) =>
|
||||
connector.enabled && (!isCustomMcp(connector) || data.allow_custom_mcp);
|
||||
const setEnabled = (connector: AdminConnector, on: boolean) =>
|
||||
save(
|
||||
isCustomMcp(connector)
|
||||
? { allow_custom_mcp: on, policies: { custom_mcp: { enabled: on } } }
|
||||
: { policies: { [connector.key]: { enabled: on } } },
|
||||
);
|
||||
|
||||
const statusBadge = (connector: AdminConnector) => (
|
||||
<Badge variant={connector.configured ? 'success' : 'warning'}>
|
||||
{connector.configured ? 'Ready' : 'Needs setup'}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
const summary = (connector: AdminConnector) =>
|
||||
[
|
||||
!connector.configured
|
||||
? 'Needs setup'
|
||||
: enabledOf(connector)
|
||||
? 'On'
|
||||
: 'Off',
|
||||
`${fmtNumber(connector.connection_count)} ${
|
||||
connector.connection_count === 1 ? 'connection' : 'connections'
|
||||
}`,
|
||||
hasTools(connector)
|
||||
? POLICY_LABELS[connector.credential_mode]
|
||||
: 'No tools',
|
||||
].join(' · ');
|
||||
|
||||
const enabledSwitch = (connector: AdminConnector) => {
|
||||
const control = (
|
||||
<Switch
|
||||
checked={enabledOf(connector)}
|
||||
disabled={!connector.configured}
|
||||
aria-label={`${connector.name} enabled`}
|
||||
onCheckedChange={(checked) => setEnabled(connector, checked === true)}
|
||||
/>
|
||||
);
|
||||
// Off until its server settings exist: switching it on would do nothing.
|
||||
if (connector.configured) return control;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span tabIndex={0} className="inline-flex w-fit">
|
||||
{control}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add its server settings first</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const policyControl = (connector: AdminConnector, fullWidth = false) =>
|
||||
hasTools(connector) ? (
|
||||
<Select
|
||||
value={connector.credential_mode}
|
||||
onValueChange={(value) =>
|
||||
save({
|
||||
policies: { [connector.key]: { credential_mode: value as Policy } },
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className={fullWidth ? 'w-full' : 'w-60'}
|
||||
aria-label={`${connector.name} sharing policy`}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(POLICY_LABELS) as Policy[]).map((policy) => (
|
||||
<SelectItem key={policy} value={policy}>
|
||||
{POLICY_LABELS[policy]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">No tools</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<PageToolbar intro="Choose which connectors members can use and whose account a shared tool runs with. A connector that still needs server settings starts turned off and is hidden from members; it turns on once its settings are in place, unless you switch it off." />
|
||||
@@ -229,22 +335,6 @@ export default function Connectors() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<SettingRows>
|
||||
<SettingRow
|
||||
label="Allow custom MCP servers"
|
||||
description="Members can connect any remote MCP server. Presets are controlled one by one below."
|
||||
htmlFor="allow-custom-mcp"
|
||||
>
|
||||
<Switch
|
||||
id="allow-custom-mcp"
|
||||
checked={data.allow_custom_mcp}
|
||||
onCheckedChange={(checked) =>
|
||||
save({ allow_custom_mcp: checked === true })
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingRows>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionHeader
|
||||
title="Redirect URIs"
|
||||
@@ -265,8 +355,37 @@ export default function Connectors() {
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionHeader title="Connectors" />
|
||||
<TableContainer>
|
||||
<SectionHeader
|
||||
title="Connectors"
|
||||
description="The custom MCP server row decides whether members can add their own MCP servers; presets are switched one by one."
|
||||
/>
|
||||
{/* Phones: a list; each row opens the connector's controls. */}
|
||||
<Card padding="none" className="overflow-hidden md:hidden">
|
||||
<ListRows>
|
||||
{data.connectors.map((connector) => (
|
||||
<ListRow
|
||||
key={connector.key}
|
||||
interactive
|
||||
asChild
|
||||
leading={
|
||||
<ConnectorIcon
|
||||
icon={connector.icon}
|
||||
className="size-5 shrink-0"
|
||||
/>
|
||||
}
|
||||
title={connector.name}
|
||||
description={summary(connector)}
|
||||
trailing={statusBadge(connector)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDetailKey(connector.key)}
|
||||
/>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListRows>
|
||||
</Card>
|
||||
<TableContainer className="hidden md:block">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -297,60 +416,12 @@ export default function Connectors() {
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={connector.configured ? 'success' : 'warning'}
|
||||
>
|
||||
{connector.configured ? 'Ready' : 'Needs setup'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{statusBadge(connector)}</TableCell>
|
||||
<TableCell align="right" className="tabular-nums">
|
||||
{fmtNumber(connector.connection_count)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={connector.enabled}
|
||||
aria-label={`${connector.name} enabled`}
|
||||
onCheckedChange={(checked) =>
|
||||
save({
|
||||
policies: {
|
||||
[connector.key]: { enabled: checked === true },
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={connector.credential_mode}
|
||||
onValueChange={(value) =>
|
||||
save({
|
||||
policies: {
|
||||
[connector.key]: {
|
||||
credential_mode: value as Policy,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-60"
|
||||
aria-label={`${connector.name} sharing policy`}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(POLICY_LABELS) as Policy[]).map(
|
||||
(policy) => (
|
||||
<SelectItem key={policy} value={policy}>
|
||||
{POLICY_LABELS[policy]}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>{enabledSwitch(connector)}</TableCell>
|
||||
<TableCell>{policyControl(connector)}</TableCell>
|
||||
<TableCell align="right">
|
||||
{connector.required_settings.length > 0 && (
|
||||
<Button
|
||||
@@ -370,6 +441,49 @@ export default function Connectors() {
|
||||
</TableContainer>
|
||||
</section>
|
||||
|
||||
{detail && (
|
||||
<Sheet open onOpenChange={(open) => !open && setDetailKey(null)}>
|
||||
<SheetContent side="right" size="detail" closeLabel="Close">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex items-center gap-3 pr-12">
|
||||
<ConnectorIcon icon={detail.icon} className="size-7" />
|
||||
<SheetTitle className="truncate">{detail.name}</SheetTitle>
|
||||
</div>
|
||||
<SheetDescription>{summary(detail)}</SheetDescription>
|
||||
<SettingRows>
|
||||
<SettingRow
|
||||
label="Enabled"
|
||||
description={
|
||||
detail.configured
|
||||
? 'Members can connect and use it.'
|
||||
: 'Add its server settings first (Setup guide).'
|
||||
}
|
||||
>
|
||||
{enabledSwitch(detail)}
|
||||
</SettingRow>
|
||||
</SettingRows>
|
||||
{hasTools(detail) && (
|
||||
<FormField label="Shared tools use">
|
||||
{policyControl(detail, true)}
|
||||
</FormField>
|
||||
)}
|
||||
{detail.required_settings.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
className="w-fit"
|
||||
onClick={() => setGuide(detail)}
|
||||
>
|
||||
Setup guide
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
|
||||
{guide && (
|
||||
<SetupGuide
|
||||
connector={guide}
|
||||
|
||||
@@ -108,6 +108,8 @@ class TestAdminPolicies:
|
||||
{"name": "GOOGLE_CLIENT_ID", "set": True}, {"name": "GOOGLE_CLIENT_SECRET", "set": False},
|
||||
]
|
||||
assert drive["connection_count"] == 1
|
||||
# Sync-only: the page shows no sharing policy for it.
|
||||
assert drive["capabilities"] == ["sync"]
|
||||
assert "secret-client-id" not in json.dumps(payload)
|
||||
assert payload["allow_custom_mcp"] is True
|
||||
|
||||
|
||||
Reference in new issue
Block a user