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
3import { expect } from "@effect/vitest";3import { expect } from "@effect/vitest";
4import { Effect } from "effect";4import { Effect } from "effect";
5import { composePluginApi } from "@executor-js/api/server";5import { composePluginApi } from "@executor-js/api/server";
6import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
6import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";7import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";
8import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
7 9
8import { scenario } from "../src/scenario";10import { scenario } from "../src/scenario";
9import { Api, Browser, Target } from "../src/services";11import { Api, Browser, Target } from "../src/services";
10 12
11const api = composePluginApi([toolkitsPlugin()] as const);13const api = composePluginApi([toolkitsPlugin(), openApiHttpPlugin()] as const);
14
15const 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 });
12 29
13scenario(30scenario(
14 "Toolkits · self-host UI creates a toolkit and configures tools",31 "Toolkits · self-host UI creates a toolkit and configures tools",
@@ -24,6 +41,8 @@ scenario( · 9 unmodified lines
24 const prefix = `toolkits-ui-${suffix}`;41 const prefix = `toolkits-ui-${suffix}`;
25 const name = `${prefix}-created`;42 const name = `${prefix}-created`;
26 const slug = name;43 const slug = name;
44 const hiddenPersonalIntegration = `${prefix}-personal-api`;
45 const hiddenPersonalConnection = "mine";
27 const seededToolkits = [46 const seededToolkits = [
28 { owner: "org" as const, name: `${prefix}-workspace-a` },47 { owner: "org" as const, name: `${prefix}-workspace-a` },
29 { owner: "org" as const, name: `${prefix}-workspace-b` },48 { owner: "org" as const, name: `${prefix}-workspace-b` },
@@ -35,6 +54,18 @@ scenario( · 5 unmodified lines
35 const blockPattern = "executor.coreTools.policies.list";54 const blockPattern = "executor.coreTools.policies.list";
36 55
37 const cleanup = Effect.gen(function* () {56 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);
38 const listed = yield* client.toolkits.list();69 const listed = yield* client.toolkits.list();
39 yield* Effect.forEach(70 yield* Effect.forEach(
40 listed.toolkits.filter((row) => row.slug.startsWith(prefix)),71 listed.toolkits.filter((row) => row.slug.startsWith(prefix)),
@@ -49,6 +80,29 @@ scenario( · 8 unmodified lines
49 (toolkit) => client.toolkits.create({ payload: toolkit }),80 (toolkit) => client.toolkits.create({ payload: toolkit }),
50 { discard: true },81 { discard: true },
51 );82 );
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 });
52 106
53 yield* browser.session(identity, async ({ page, step }) => {107 yield* browser.session(identity, async ({ page, step }) => {
54 await step("Open the Toolkits plugin page", async () => {108 await step("Open the Toolkits plugin page", async () => {
@@ -143,8 +197,18 @@ scenario( · 88 unmodified lines
143 await page.getByRole("heading", { name }).waitFor();197 await page.getByRole("heading", { name }).waitFor();
144 });198 });
145 199
146 await step("Add a connection to the toolkit", async () => {200 await step("The connection picker explains hidden personal connections", async () => {
147 await page.getByRole("button", { name: "Manage toolkit connections" }).click();201 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 () => {
148 const dialog = page.getByRole("dialog", { name: "Manage connections" });212 const dialog = page.getByRole("dialog", { name: "Manage connections" });
149 await dialog.waitFor();213 await dialog.waitFor();
150 await dialog.getByLabel("Search connections and tools").fill("policies.list");214 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
763 open: boolean;763 open: boolean;
764 groups: readonly ToolkitConnectionGroup[];764 groups: readonly ToolkitConnectionGroup[];
765 configuredConnections: readonly ConfiguredConnectionView[];765 configuredConnections: readonly ConfiguredConnectionView[];
766 hiddenPersonalConnectionCount: number;
766 integrations: readonly Integration[];767 integrations: readonly Integration[];
767 integrationPlugins: readonly IntegrationPlugin[];768 integrationPlugins: readonly IntegrationPlugin[];
768 showOwnerLabels: boolean;769 showOwnerLabels: boolean;
@@ -843,6 +844,14 @@ function AddConnectionDialog(props: { · 74 unmodified lines
843 className="h-8 min-w-0 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"844 className="h-8 min-w-0 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
844 />845 />
845 </div>846 </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}
846 </div>855 </div>
847 <div className="min-h-0 overflow-y-auto p-3" style={{ maxHeight: "32rem" }}>856 <div className="min-h-0 overflow-y-auto p-3" style={{ maxHeight: "32rem" }}>
848 <div className="mb-2 flex items-center justify-between px-1">857 <div className="mb-2 flex items-center justify-between px-1">
@@ -1045,6 +1054,10 @@ function ToolkitWorkspace(props: { · 196 unmodified lines
1045 [props.toolkit, props.tools],1054 [props.toolkit, props.tools],
1046 );1055 );
1047 const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]);1056 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]);
1048 const configuredConnections = useMemo(1061 const configuredConnections = useMemo(
1049 () =>1062 () =>
1050 configuredConnectionViews(1063 configuredConnectionViews(
@@ -1153,6 +1166,7 @@ function ToolkitWorkspace(props: { · 102 unmodified lines
1153 onOpenChange={setAddOpen}1166 onOpenChange={setAddOpen}
1154 groups={connectionGroups}1167 groups={connectionGroups}
1155 configuredConnections={configuredConnections}1168 configuredConnections={configuredConnections}
1169 hiddenPersonalConnectionCount={hiddenPersonalConnectionCount}
1156 integrations={props.integrations}1170 integrations={props.integrations}
1157 integrationPlugins={props.integrationPlugins}1171 integrationPlugins={props.integrationPlugins}
1158 showOwnerLabels={props.showOwnerLabels}1172 showOwnerLabels={props.showOwnerLabels}