Files
cobble/src/pages/admin/settings/(partials)/extension-access-modal.tsx
T
tbenandClaude Opus 5.5 6294b911e6 Give bots tools and MCP servers per bot and workspace, with status and tests
- Bots and workspaces get tools and MCP servers from their settings
  pages; bots also from chips under the chat composer, removable there
- "Every bot" now gives one to every bot now and every new bot; each can
  still drop it (attach-every-bot-extensions.ts moves old installs over)
- Status lines: MCP servers record whether they connect, tools whether
  their last run worked
- Test connection in the MCP form and a Test panel in the tool form check
  the form as it is, without saving
- Hide Google sign-in on the login form

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:35:13 +01:00

123 lines
4.6 KiB
TypeScript

import { useState } from "react";
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
import Checkbox from "@moduletrace/twui/form/Checkbox";
import Form from "@moduletrace/twui/form/Form";
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
import type { ExtensionAccess, ExtensionTargets } from "@/src/types";
type Props<T> = {
/**
* The MCP server's or tool's id and name.
*/
id: number;
name: string;
access: ExtensionAccess;
targets: ExtensionTargets;
/**
* Under `/api/admin/`.
*/
path: "mcps/set-mcp-access" | "tools/set-tool-access";
onSaved: (overview: T, msg: string) => void;
onCancel: () => void;
};
/**
* Who gets an MCP server or tool: the bots and workspaces picked here.
* "Every bot" (super admins only) gives it to every bot now and every new
* one; each can still drop it. A workspace gives it to every bot in
* it, there.
*/
export default function ExtensionAccessModal<T extends { [k: string]: any }>({
id,
name,
access,
targets,
path,
onSaved,
onCancel,
}: Props<T>) {
const [all_bots, setAllBots] = useState(access.all_bots);
const [bot_ids, setBotIds] = useState(access.bot_ids);
const [workspace_ids, setWorkspaceIds] = useState(access.workspace_ids);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const submit = async () => {
if (loading || !window.confirm(`Save who gets ${name}?`)) return;
setLoading(true);
setError(undefined);
const res = await postAdminApi<T>({
path,
body: { id, bot_ids, workspace_ids, all_bots: targets.can_edit ? all_bots : undefined },
});
setLoading(false);
if (!res.success || !res.singleRes) {
setError(res.msg || "Couldn't save who gets it.");
return;
}
onSaved(res.singleRes, res.msg || "Saved.");
};
const ids = (value: unknown) => (Array.isArray(value) ? value : value ? [value] : []).map(Number);
return (
<AddModalShell
open
setOpen={(next) => !next && onCancel()}
title={`Who gets ${name}`}
description={"A bot gets it everywhere it works; a workspace gives it to every bot in it, there."}
>
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={submit}>
{targets.can_edit ? (
<Checkbox
title="Every bot"
label="Every bot by default: every bot now and every new one, the coordinator included. Each can still drop it."
checked={all_bots}
changeHandler={setAllBots}
/>
) : access.all_bots ? (
<Checkbox title="Every bot" label="Every bot by default (only a super admin changes this)" checked readOnly />
) : null}
<SearchSelect
id={`${path}-${id}-bots`}
title="Bots"
label="Bots"
showLabel
multiple
placeholder="Pick the bots that get it"
options={targets.bots.map((bot) => ({
value: String(bot.id),
title: bot.role ? `${bot.name} (${bot.role})` : bot.name,
}))}
defaultValues={bot_ids.map(String)}
info="They get it everywhere they work."
changeHandler={(value) => setBotIds(ids(value))}
/>
<SearchSelect
id={`${path}-${id}-workspaces`}
title="Workspaces"
label="Workspaces"
showLabel
multiple
placeholder="Pick the workspaces that get it"
options={targets.workspaces.map((workspace) => ({
value: String(workspace.id),
title: workspace.name,
}))}
defaultValues={workspace_ids.map(String)}
info="Every bot in these gets it, there."
changeHandler={(value) => setWorkspaceIds(ids(value))}
/>
<AddFormActions submit_label="Save" loading={loading} error={error} onCancel={onCancel} />
</Form>
</AddModalShell>
);
}