History

Explain hidden personal toolkit connections

Rhys Sullivan committed 4 months ago

Commit 21e32061 parent 2d221ab2 files · +80 −2Browse files

Unified · Split

2 files
  1. e2e/selfhost/toolkits-ui.test.ts +66 −2
  2. packages/plugins/toolkits/src/page.tsx +14 −0
e2e/selfhost/toolkits-ui.test.ts+66−2
@@ -3,12 +3,29 @@ import { randomBytes } from "node:crypto"; · 2 unmodified lines
33 import { expect } from "@effect/vitest";
44 import { Effect } from "effect";
55 import { composePluginApi } from "@executor-js/api/server";
6+import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
67 import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";
8+import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
79
810 import { scenario } from "../src/scenario";
911 import { Api, Browser, Target } from "../src/services";
1012
11−const api = composePluginApi([toolkitsPlugin()] as const);
13+const api = composePluginApi([toolkitsPlugin(), openApiHttpPlugin()] as const);
14+
15+const hiddenPersonalSpec = (baseUrl: string): string =>
16+ JSON.stringify({
17+ openapi: "3.0.3",
18+ info: { title: "Personal Hidden API", version: "1.0.0" },
19+ servers: [{ url: baseUrl }],
20+ paths: {
21+ "/personal-only": {
22+ get: {
23+ operationId: "personalOnly",
24+ responses: { "200": { description: "Personal-only response" } },
25+ },
26+ },
27+ },
28+ });
1229
1330 scenario(
1431 "Toolkits · self-host UI creates a toolkit and configures tools",
@@ -24,6 +41,8 @@ scenario( · 9 unmodified lines
2441 const prefix = `toolkits-ui-${suffix}`;
2542 const name = `${prefix}-created`;
2643 const slug = name;
44+ const hiddenPersonalIntegration = `${prefix}-personal-api`;
45+ const hiddenPersonalConnection = "mine";
2746 const seededToolkits = [
2847 { owner: "org" as const, name: `${prefix}-workspace-a` },
2948 { owner: "org" as const, name: `${prefix}-workspace-b` },
@@ -35,6 +54,18 @@ scenario( · 5 unmodified lines
3554 const blockPattern = "executor.coreTools.policies.list";
3655
3756 const cleanup = Effect.gen(function* () {
57+ yield* client.connections
58+ .remove({
59+ params: {
60+ owner: "user",
61+ integration: IntegrationSlug.make(hiddenPersonalIntegration),
62+ name: ConnectionName.make(hiddenPersonalConnection),
63+ },
64+ })
65+ .pipe(Effect.ignore);
66+ yield* client.openapi
67+ .removeSpec({ params: { slug: hiddenPersonalIntegration } })
68+ .pipe(Effect.ignore);
3869 const listed = yield* client.toolkits.list();
3970 yield* Effect.forEach(
4071 listed.toolkits.filter((row) => row.slug.startsWith(prefix)),
@@ -49,6 +80,29 @@ scenario( · 8 unmodified lines
4980 (toolkit) => client.toolkits.create({ payload: toolkit }),
5081 { discard: true },
5182 );
83+ yield* client.openapi.addSpec({
84+ payload: {
85+ spec: { kind: "blob", value: hiddenPersonalSpec("http://127.0.0.1:59999") },
86+ slug: IntegrationSlug.make(hiddenPersonalIntegration),
87+ baseUrl: "http://127.0.0.1:59999",
88+ authenticationTemplate: [
89+ {
90+ slug: "apiKey",
91+ type: "apiKey",
92+ headers: { "x-api-key": [{ type: "variable", name: "token" }] },
93+ },
94+ ],
95+ },
96+ });
97+ yield* client.connections.create({
98+ payload: {
99+ owner: "user",
100+ name: ConnectionName.make(hiddenPersonalConnection),
101+ integration: IntegrationSlug.make(hiddenPersonalIntegration),
102+ template: AuthTemplateSlug.make("apiKey"),
103+ value: "unused-token",
104+ },
105+ });
52106
53107 yield* browser.session(identity, async ({ page, step }) => {
54108 await step("Open the Toolkits plugin page", async () => {
@@ -143,8 +197,18 @@ scenario( · 88 unmodified lines
143197 await page.getByRole("heading", { name }).waitFor();
144198 });
145199
146− await step("Add a connection to the toolkit", async () => {
200+ await step("The connection picker explains hidden personal connections", async () => {
147201 await page.getByRole("button", { name: "Manage toolkit connections" }).click();
202+ const dialog = page.getByRole("dialog", { name: "Manage connections" });
203+ await dialog.waitFor();
204+ await dialog
205+ .getByText(
206+ "You have 1 personal connection that is not shown because this is a shared toolkit.",
207+ )
208+ .waitFor();
209+ });
210+
211+ await step("Add a connection to the toolkit", async () => {
148212 const dialog = page.getByRole("dialog", { name: "Manage connections" });
149213 await dialog.waitFor();
150214 await dialog.getByLabel("Search connections and tools").fill("policies.list");
packages/plugins/toolkits/src/page.tsx+14−0
@@ -763,6 +763,7 @@ function AddConnectionDialog(props: { · 762 unmodified lines
763763 open: boolean;
764764 groups: readonly ToolkitConnectionGroup[];
765765 configuredConnections: readonly ConfiguredConnectionView[];
766+ hiddenPersonalConnectionCount: number;
766767 integrations: readonly Integration[];
767768 integrationPlugins: readonly IntegrationPlugin[];
768769 showOwnerLabels: boolean;
@@ -843,6 +844,14 @@ function AddConnectionDialog(props: { · 74 unmodified lines
843844 className="h-8 min-w-0 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
844845 />
845846 </div>
847+ {props.hiddenPersonalConnectionCount > 0 ? (
848+ <p className="mt-2 px-1 text-xs leading-5 text-muted-foreground">
849+ You have {props.hiddenPersonalConnectionCount} personal{" "}
850+ {props.hiddenPersonalConnectionCount === 1 ? "connection" : "connections"} that{" "}
851+ {props.hiddenPersonalConnectionCount === 1 ? "is" : "are"} not shown because this is
852+ a shared toolkit.
853+ </p>
854+ ) : null}
846855 </div>
847856 <div className="min-h-0 overflow-y-auto p-3" style={{ maxHeight: "32rem" }}>
848857 <div className="mb-2 flex items-center justify-between px-1">
@@ -1045,6 +1054,10 @@ function ToolkitWorkspace(props: { · 196 unmodified lines
10451054 [props.toolkit, props.tools],
10461055 );
10471056 const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]);
1057+ const hiddenPersonalConnectionCount = useMemo(() => {
1058+ if (props.toolkit.owner !== "org") return 0;
1059+ return buildConnectionGroups(props.tools.filter((tool) => toolOwner(tool) === "user")).length;
1060+ }, [props.toolkit.owner, props.tools]);
10481061 const configuredConnections = useMemo(
10491062 () =>
10501063 configuredConnectionViews(
@@ -1153,6 +1166,7 @@ function ToolkitWorkspace(props: { · 102 unmodified lines
11531166 onOpenChange={setAddOpen}
11541167 groups={connectionGroups}
11551168 configuredConnections={configuredConnections}
1169+ hiddenPersonalConnectionCount={hiddenPersonalConnectionCount}
11561170 integrations={props.integrations}
11571171 integrationPlugins={props.integrationPlugins}
11581172 showOwnerLabels={props.showOwnerLabels}