@stigmer/react 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/useAgentCount.d.ts +2 -0
- package/agent/useAgentCount.d.ts.map +1 -1
- package/agent/useAgentCount.js.map +1 -1
- package/api-key/CreateApiKeyForm.d.ts +7 -1
- package/api-key/CreateApiKeyForm.d.ts.map +1 -1
- package/api-key/CreateApiKeyForm.js +2 -2
- package/api-key/CreateApiKeyForm.js.map +1 -1
- package/datastore/useDatastoreCount.d.ts +2 -0
- package/datastore/useDatastoreCount.d.ts.map +1 -1
- package/datastore/useDatastoreCount.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/manifest/ApplyManifestDialog.d.ts +10 -5
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +30 -13
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/manifest/useApplyManifest.d.ts +6 -2
- package/manifest/useApplyManifest.d.ts.map +1 -1
- package/manifest/useApplyManifest.js +5 -5
- package/manifest/useApplyManifest.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts +38 -2
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +21 -5
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/index.d.ts +7 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +9 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
- package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.js +7 -1
- package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
- package/mcp-server/steps/ReviewStep.d.ts +5 -0
- package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
- package/mcp-server/steps/ReviewStep.js +16 -2
- package/mcp-server/steps/ReviewStep.js.map +1 -1
- package/mcp-server/useMcpServerCount.d.ts +2 -0
- package/mcp-server/useMcpServerCount.d.ts.map +1 -1
- package/mcp-server/useMcpServerCount.js.map +1 -1
- package/package.json +4 -4
- package/resource-creation/CreationPicker.d.ts.map +1 -1
- package/resource-creation/CreationPicker.js +3 -3
- package/resource-creation/CreationPicker.js.map +1 -1
- package/resource-creation/WizardNav.d.ts.map +1 -1
- package/resource-creation/WizardNav.js +1 -1
- package/resource-creation/WizardNav.js.map +1 -1
- package/resource-detail/Section.d.ts +8 -1
- package/resource-detail/Section.d.ts.map +1 -1
- package/resource-detail/Section.js +2 -2
- package/resource-detail/Section.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +2 -1
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/search/useResourceCount.d.ts +7 -0
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +2 -1
- package/search/useResourceCount.js.map +1 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +17 -3
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +2 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +19 -11
- package/session/SessionViewer.js.map +1 -1
- package/session/SessionViewerLayout.d.ts +90 -0
- package/session/SessionViewerLayout.d.ts.map +1 -0
- package/session/SessionViewerLayout.js +32 -0
- package/session/SessionViewerLayout.js.map +1 -0
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +10 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +2 -2
- package/skill/useSkillCount.d.ts +2 -0
- package/skill/useSkillCount.d.ts.map +1 -1
- package/skill/useSkillCount.js.map +1 -1
- package/src/agent/useAgentCount.ts +2 -0
- package/src/api-key/CreateApiKeyForm.tsx +8 -1
- package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
- package/src/datastore/useDatastoreCount.ts +2 -0
- package/src/index.ts +11 -0
- package/src/manifest/ApplyManifestDialog.tsx +40 -16
- package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
- package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
- package/src/manifest/useApplyManifest.ts +12 -8
- package/src/mcp-server/McpServerDetailView.tsx +52 -4
- package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
- package/src/mcp-server/index.ts +13 -0
- package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
- package/src/mcp-server/steps/IdentityTransportStep.tsx +9 -1
- package/src/mcp-server/steps/ReviewStep.tsx +18 -1
- package/src/mcp-server/useMcpServerCount.ts +2 -0
- package/src/resource-creation/CreationPicker.tsx +7 -0
- package/src/resource-creation/WizardNav.tsx +1 -0
- package/src/resource-detail/Section.tsx +9 -1
- package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/search/useResourceCount.ts +9 -1
- package/src/session/NewSessionViewer.tsx +48 -61
- package/src/session/SessionViewer.tsx +71 -86
- package/src/session/SessionViewerLayout.tsx +131 -0
- package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
- package/src/session/index.ts +14 -0
- package/src/session/useSessionRailViews.tsx +2 -2
- package/src/skill/useSkillCount.ts +2 -0
- package/src/test/__tests__/samples.test.ts +159 -2
- package/src/test/index.ts +1 -1
- package/src/test/samples.ts +109 -14
- package/src/workflow/useWorkflowCount.ts +2 -0
- package/test/index.d.ts +1 -1
- package/test/index.d.ts.map +1 -1
- package/test/index.js +1 -1
- package/test/index.js.map +1 -1
- package/test/samples.d.ts +79 -7
- package/test/samples.d.ts.map +1 -1
- package/test/samples.js +101 -14
- package/test/samples.js.map +1 -1
- package/workflow/useWorkflowCount.d.ts +2 -0
- package/workflow/useWorkflowCount.d.ts.map +1 -1
- package/workflow/useWorkflowCount.js.map +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent, within } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import { createRouterTransport, ConnectError, Code } from "@connectrpc/connect";
|
|
7
|
+
import { Stigmer } from "@stigmer/sdk";
|
|
8
|
+
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
9
|
+
import {
|
|
10
|
+
McpServerSpecSchema,
|
|
11
|
+
HttpServerConfigSchema,
|
|
12
|
+
ToolApprovalPolicySchema,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
14
|
+
import {
|
|
15
|
+
McpServerStatusSchema,
|
|
16
|
+
DiscoveredCapabilitiesSchema,
|
|
17
|
+
DiscoveredToolSchema,
|
|
18
|
+
ValidationState,
|
|
19
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
20
|
+
import { EnvVarDeclarationSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
21
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
22
|
+
import { samples } from "../../test/samples";
|
|
23
|
+
import { StigmerContext } from "../../context";
|
|
24
|
+
import { McpServerDetailView } from "../McpServerDetailView";
|
|
25
|
+
import type { UseMcpServerReturn } from "../useMcpServer";
|
|
26
|
+
|
|
27
|
+
// The kebab menu and dialogs portal their content; without a StigmerProvider
|
|
28
|
+
// the portal container is null — pin it to document.body like sibling tests.
|
|
29
|
+
vi.mock("../../portal-container", () => ({
|
|
30
|
+
useStigmerPortalContainer: () => document.body,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
// happy-dom lacks ResizeObserver, which Base UI positioners observe.
|
|
35
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
36
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
37
|
+
class {
|
|
38
|
+
observe() {}
|
|
39
|
+
unobserve() {}
|
|
40
|
+
disconnect() {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(cleanup);
|
|
46
|
+
|
|
47
|
+
const ORG = "acme";
|
|
48
|
+
const SLUG = "order-management-api";
|
|
49
|
+
|
|
50
|
+
/** Frozen discovery instant — fixtures must never read the real clock. */
|
|
51
|
+
const DISCOVERED_AT = new Date("2026-07-20T09:30:00Z");
|
|
52
|
+
|
|
53
|
+
/** The server as registered: HTTP transport, one secret env var, no tools. */
|
|
54
|
+
function buildBaseServer(): McpServer {
|
|
55
|
+
const server = samples.mcpServer({ name: "Order Management API", org: ORG, slug: SLUG });
|
|
56
|
+
server.spec = create(McpServerSpecSchema, {
|
|
57
|
+
description: "REST API for order lookup, inventory, and return processing.",
|
|
58
|
+
serverType: {
|
|
59
|
+
case: "http",
|
|
60
|
+
value: create(HttpServerConfigSchema, { url: "https://api.acme.com/mcp" }),
|
|
61
|
+
},
|
|
62
|
+
env: {
|
|
63
|
+
API_TOKEN: create(EnvVarDeclarationSchema, {
|
|
64
|
+
isSecret: true,
|
|
65
|
+
description: "Bearer token for the Order Management API",
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return server;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The same server after Connect: 3 discovered tools, 1 classified policy. */
|
|
73
|
+
function buildConnectedServer(): McpServer {
|
|
74
|
+
const server = buildBaseServer();
|
|
75
|
+
server.status = create(McpServerStatusSchema, {
|
|
76
|
+
validationState: ValidationState.valid,
|
|
77
|
+
discoveredCapabilities: create(DiscoveredCapabilitiesSchema, {
|
|
78
|
+
lastDiscoveredAt: timestampFromDate(DISCOVERED_AT),
|
|
79
|
+
tools: [
|
|
80
|
+
create(DiscoveredToolSchema, { name: "get_order", description: "Retrieve one order by ID." }),
|
|
81
|
+
create(DiscoveredToolSchema, { name: "list_orders", description: "List recent orders." }),
|
|
82
|
+
create(DiscoveredToolSchema, { name: "process_return", description: "Initiate a return and refund." }),
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
toolApprovals: [
|
|
86
|
+
create(ToolApprovalPolicySchema, {
|
|
87
|
+
toolName: "process_return",
|
|
88
|
+
message: "Process return for order '{{args.order_id}}'",
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
return server;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Hoisted-state bag in its settled, data-loaded shape. */
|
|
96
|
+
function loadedState(server: McpServer): UseMcpServerReturn {
|
|
97
|
+
return { mcpServer: server, isLoading: false, isRefetching: false, error: null, refetch: vi.fn() };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mounts the view against a real Connect router transport. RPCs the test
|
|
102
|
+
* does not register fall through to Connect's `unimplemented`, which the
|
|
103
|
+
* SDK hooks degrade from — the same contract the tours rely on.
|
|
104
|
+
*/
|
|
105
|
+
function renderView(
|
|
106
|
+
ui: ReactNode,
|
|
107
|
+
register: Parameters<typeof createRouterTransport>[0] = () => {},
|
|
108
|
+
) {
|
|
109
|
+
const client = new Stigmer({
|
|
110
|
+
baseUrl: "/",
|
|
111
|
+
getAccessToken: () => "test-token",
|
|
112
|
+
customTransport: createRouterTransport(register),
|
|
113
|
+
});
|
|
114
|
+
return render(
|
|
115
|
+
<StigmerContext.Provider value={client}>{ui}</StigmerContext.Provider>,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("McpServerDetailView — hoisted mcpServerState", () => {
|
|
120
|
+
it("renders from injected state without issuing getByReference", async () => {
|
|
121
|
+
const getByReference = vi.fn(() => {
|
|
122
|
+
throw new ConnectError("must not fetch", Code.FailedPrecondition);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
renderView(
|
|
126
|
+
<McpServerDetailView
|
|
127
|
+
org={ORG}
|
|
128
|
+
slug={SLUG}
|
|
129
|
+
mcpServerState={loadedState(buildBaseServer())}
|
|
130
|
+
/>,
|
|
131
|
+
(router) => {
|
|
132
|
+
router.service(McpServerQueryController, { getByReference });
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Content is on screen synchronously — no skeleton frame first.
|
|
137
|
+
expect(
|
|
138
|
+
screen.getByRole("heading", { name: "Order Management API" }),
|
|
139
|
+
).toBeTruthy();
|
|
140
|
+
expect(screen.queryByLabelText("Loading MCP server details")).toBeNull();
|
|
141
|
+
|
|
142
|
+
// Let pending microtasks (credentials/permission fetches) settle, then
|
|
143
|
+
// confirm the resource fetch never fired.
|
|
144
|
+
await screen.findByRole("tab", { name: /Tools/ });
|
|
145
|
+
expect(getByReference).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("shows the loading skeleton while the injected state is loading", () => {
|
|
149
|
+
renderView(
|
|
150
|
+
<McpServerDetailView
|
|
151
|
+
org={ORG}
|
|
152
|
+
slug={SLUG}
|
|
153
|
+
mcpServerState={{
|
|
154
|
+
mcpServer: null,
|
|
155
|
+
isLoading: true,
|
|
156
|
+
isRefetching: false,
|
|
157
|
+
error: null,
|
|
158
|
+
refetch: vi.fn(),
|
|
159
|
+
}}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
expect(screen.getByLabelText("Loading MCP server details")).toBeTruthy();
|
|
163
|
+
// A loading owner must never read as a missing resource.
|
|
164
|
+
expect(screen.queryByText("MCP Server not found")).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("routes the error state's Retry to the injected refetch", () => {
|
|
168
|
+
const refetch = vi.fn();
|
|
169
|
+
renderView(
|
|
170
|
+
<McpServerDetailView
|
|
171
|
+
org={ORG}
|
|
172
|
+
slug={SLUG}
|
|
173
|
+
mcpServerState={{
|
|
174
|
+
mcpServer: null,
|
|
175
|
+
isLoading: false,
|
|
176
|
+
isRefetching: false,
|
|
177
|
+
error: new ConnectError("backend down", Code.Unavailable),
|
|
178
|
+
refetch,
|
|
179
|
+
}}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
fireEvent.click(screen.getByRole("button", { name: /retry/i }));
|
|
184
|
+
expect(refetch).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("renders not-found when the injected state settles with no resource", () => {
|
|
188
|
+
renderView(
|
|
189
|
+
<McpServerDetailView
|
|
190
|
+
org={ORG}
|
|
191
|
+
slug={SLUG}
|
|
192
|
+
mcpServerState={{
|
|
193
|
+
mcpServer: null,
|
|
194
|
+
isLoading: false,
|
|
195
|
+
isRefetching: false,
|
|
196
|
+
error: null,
|
|
197
|
+
refetch: vi.fn(),
|
|
198
|
+
}}
|
|
199
|
+
/>,
|
|
200
|
+
);
|
|
201
|
+
expect(screen.getByText("MCP Server not found")).toBeTruthy();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("McpServerDetailView — self-fetching mode (unchanged behaviour)", () => {
|
|
206
|
+
it("fetches by org/slug and renders the resolved server", async () => {
|
|
207
|
+
const getByReference = vi.fn(() => buildBaseServer());
|
|
208
|
+
renderView(<McpServerDetailView org={ORG} slug={SLUG} />, (router) => {
|
|
209
|
+
router.service(McpServerQueryController, { getByReference });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// First paint is the skeleton; content follows the fetch.
|
|
213
|
+
expect(screen.getByLabelText("Loading MCP server details")).toBeTruthy();
|
|
214
|
+
expect(
|
|
215
|
+
await screen.findByRole("heading", { name: "Order Management API" }),
|
|
216
|
+
).toBeTruthy();
|
|
217
|
+
expect(getByReference).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("renders not-found when the fetch 404s", async () => {
|
|
221
|
+
renderView(<McpServerDetailView org={ORG} slug={SLUG} />, (router) => {
|
|
222
|
+
router.service(McpServerQueryController, {
|
|
223
|
+
getByReference: () => {
|
|
224
|
+
throw new ConnectError("no such server", Code.NotFound);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
expect(await screen.findByText("MCP Server not found")).toBeTruthy();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("McpServerDetailView — connect bar and capability tabs", () => {
|
|
233
|
+
it("offers Connect with the not-connected status before discovery", () => {
|
|
234
|
+
renderView(
|
|
235
|
+
<McpServerDetailView
|
|
236
|
+
org={ORG}
|
|
237
|
+
slug={SLUG}
|
|
238
|
+
mcpServerState={loadedState(buildBaseServer())}
|
|
239
|
+
/>,
|
|
240
|
+
);
|
|
241
|
+
expect(screen.getByRole("button", { name: /^Connect$/ })).toBeTruthy();
|
|
242
|
+
expect(screen.getByText("Not connected yet")).toBeTruthy();
|
|
243
|
+
expect(
|
|
244
|
+
screen.getByText("Connect to this MCP server to discover its available tools."),
|
|
245
|
+
).toBeTruthy();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("offers Reconnect with the connected status and discovery date after discovery", () => {
|
|
249
|
+
renderView(
|
|
250
|
+
<McpServerDetailView
|
|
251
|
+
org={ORG}
|
|
252
|
+
slug={SLUG}
|
|
253
|
+
mcpServerState={loadedState(buildConnectedServer())}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
expect(screen.getByRole("button", { name: /Reconnect/ })).toBeTruthy();
|
|
257
|
+
expect(screen.getByText("Connected")).toBeTruthy();
|
|
258
|
+
expect(screen.getByText("Valid")).toBeTruthy();
|
|
259
|
+
expect(screen.getByText(/Discovered/)).toBeTruthy();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("lists the discovered tools with the count badge", () => {
|
|
263
|
+
renderView(
|
|
264
|
+
<McpServerDetailView
|
|
265
|
+
org={ORG}
|
|
266
|
+
slug={SLUG}
|
|
267
|
+
mcpServerState={loadedState(buildConnectedServer())}
|
|
268
|
+
/>,
|
|
269
|
+
);
|
|
270
|
+
const toolsTab = screen.getByRole("tab", { name: /Tools/ });
|
|
271
|
+
expect(toolsTab.textContent).toContain("3");
|
|
272
|
+
expect(screen.getByText("get_order")).toBeTruthy();
|
|
273
|
+
expect(screen.getByText("list_orders")).toBeTruthy();
|
|
274
|
+
expect(screen.getByText("process_return")).toBeTruthy();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("groups the classified policy under Auto-classified with a badge of 1, not 3", () => {
|
|
278
|
+
renderView(
|
|
279
|
+
<McpServerDetailView
|
|
280
|
+
org={ORG}
|
|
281
|
+
slug={SLUG}
|
|
282
|
+
defaultCapabilityTab="policies"
|
|
283
|
+
mcpServerState={loadedState(buildConnectedServer())}
|
|
284
|
+
/>,
|
|
285
|
+
);
|
|
286
|
+
const policiesTab = screen.getByRole("tab", { name: /Policies/ });
|
|
287
|
+
expect(policiesTab.textContent).toContain("1");
|
|
288
|
+
expect(policiesTab.textContent).not.toContain("3");
|
|
289
|
+
|
|
290
|
+
const panel = screen.getByRole("tabpanel");
|
|
291
|
+
expect(within(panel).getByText(/Auto-classified/)).toBeTruthy();
|
|
292
|
+
expect(within(panel).getByText("process_return")).toBeTruthy();
|
|
293
|
+
expect(within(panel).getByText("requires approval")).toBeTruthy();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("McpServerDetailView — tour scroll anchors", () => {
|
|
298
|
+
it("marks the Connection and Capabilities sections as scroll targets", () => {
|
|
299
|
+
const { container } = renderView(
|
|
300
|
+
<McpServerDetailView
|
|
301
|
+
org={ORG}
|
|
302
|
+
slug={SLUG}
|
|
303
|
+
mcpServerState={loadedState(buildBaseServer())}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
expect(
|
|
307
|
+
container.querySelector('[data-scroll-target="mcp-connection"]'),
|
|
308
|
+
).toBeTruthy();
|
|
309
|
+
expect(
|
|
310
|
+
container.querySelector('[data-scroll-target="mcp-capabilities"]'),
|
|
311
|
+
).toBeTruthy();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("McpServerDetailView — credential form gating", () => {
|
|
316
|
+
it("opens the credential form for the declared missing variable when asked", async () => {
|
|
317
|
+
renderView(
|
|
318
|
+
<McpServerDetailView
|
|
319
|
+
org={ORG}
|
|
320
|
+
slug={SLUG}
|
|
321
|
+
defaultShowCredentialForm
|
|
322
|
+
mcpServerState={loadedState(buildBaseServer())}
|
|
323
|
+
/>,
|
|
324
|
+
);
|
|
325
|
+
expect(await screen.findByText("Credentials Required")).toBeTruthy();
|
|
326
|
+
// Scope to the credential form — the Environment section also lists
|
|
327
|
+
// API_TOKEN as a declaration.
|
|
328
|
+
const form = screen.getByRole("form", {
|
|
329
|
+
name: /Configure Credentials Required/i,
|
|
330
|
+
});
|
|
331
|
+
// Anchored: the secret input's reveal button is labelled "Show API_TOKEN".
|
|
332
|
+
expect(within(form).getByLabelText(/^API_TOKEN/)).toBeTruthy();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("keeps the form closed by default", () => {
|
|
336
|
+
renderView(
|
|
337
|
+
<McpServerDetailView
|
|
338
|
+
org={ORG}
|
|
339
|
+
slug={SLUG}
|
|
340
|
+
mcpServerState={loadedState(buildBaseServer())}
|
|
341
|
+
/>,
|
|
342
|
+
);
|
|
343
|
+
expect(screen.queryByText("Credentials Required")).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
});
|
package/src/mcp-server/index.ts
CHANGED
|
@@ -100,4 +100,17 @@ export type {
|
|
|
100
100
|
McpServerCreationResult,
|
|
101
101
|
} from "./McpServerCreationWizard.js";
|
|
102
102
|
|
|
103
|
+
// Individual wizard steps are exported as presentational building blocks:
|
|
104
|
+
// they are fully prop-driven (data, validationError, error, isCreating),
|
|
105
|
+
// which lets platform builders and guided tours/demos render any wizard
|
|
106
|
+
// state — including validation and submit failures — deterministically,
|
|
107
|
+
// without driving the stateful McpServerCreationWizard through user events.
|
|
108
|
+
export { IdentityTransportStep } from "./steps/IdentityTransportStep.js";
|
|
109
|
+
export type { IdentityTransportStepProps } from "./steps/IdentityTransportStep.js";
|
|
110
|
+
export { EnvironmentAuthStep } from "./steps/EnvironmentAuthStep.js";
|
|
111
|
+
export type { EnvironmentAuthStepProps } from "./steps/EnvironmentAuthStep.js";
|
|
112
|
+
export { ReviewStep } from "./steps/ReviewStep.js";
|
|
113
|
+
export type { ReviewStepProps } from "./steps/ReviewStep.js";
|
|
114
|
+
|
|
115
|
+
export { createInitialMcpServerWizardData } from "./steps/types.js";
|
|
103
116
|
export type { McpServerWizardData } from "./steps/types.js";
|
|
@@ -16,6 +16,10 @@ export interface EnvironmentAuthStepProps {
|
|
|
16
16
|
* Both sections are optional. Env vars start expanded if any exist;
|
|
17
17
|
* auth starts collapsed unless already enabled. No validation gate —
|
|
18
18
|
* the user can proceed with zero configuration.
|
|
19
|
+
*
|
|
20
|
+
* Fully presentational: form state lives in the `data` prop and edits
|
|
21
|
+
* flow out through `updateData`, so standalone consumers (embedded
|
|
22
|
+
* builders, guided tours) can render any configuration state from props.
|
|
19
23
|
*/
|
|
20
24
|
export function EnvironmentAuthStep({
|
|
21
25
|
data,
|
|
@@ -23,6 +23,12 @@ const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
23
23
|
*
|
|
24
24
|
* Transport type uses a radio group (mutually exclusive modes).
|
|
25
25
|
* Conditional fields render based on the selected transport.
|
|
26
|
+
*
|
|
27
|
+
* Fully presentational: state lives in the `data` prop and every edit
|
|
28
|
+
* flows out through `updateData`. Inside `McpServerCreationWizard` that
|
|
29
|
+
* state comes from `useWizardState`; standalone consumers (embedded
|
|
30
|
+
* builders, guided tours) can render any form state — including a
|
|
31
|
+
* `validationError` — deterministically from props.
|
|
26
32
|
*/
|
|
27
33
|
export function IdentityTransportStep({
|
|
28
34
|
data,
|
|
@@ -195,7 +201,9 @@ export function IdentityTransportStep({
|
|
|
195
201
|
</fieldset>
|
|
196
202
|
</div>
|
|
197
203
|
|
|
198
|
-
|
|
204
|
+
{/* data-scroll-target: guided tours/demos scroll the transport config
|
|
205
|
+
(the step's decision point) into view within the wizard's scroll area. */}
|
|
206
|
+
<fieldset className="space-y-3" data-scroll-target="mcp-transport">
|
|
199
207
|
<legend className="text-sm font-medium text-foreground">
|
|
200
208
|
Transport <span className="text-destructive">*</span>
|
|
201
209
|
</legend>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import { buildMcpServerProto, getUserMessage, serializeManifest } from "@stigmer/sdk";
|
|
6
6
|
import type { McpServerInput } from "@stigmer/sdk";
|
|
@@ -20,6 +20,11 @@ export interface ReviewStepProps {
|
|
|
20
20
|
* Shows a summary card with key configuration details and a full
|
|
21
21
|
* YAML preview of the MCP server that will be created. The "Create"
|
|
22
22
|
* action is in the WizardNav footer, not in this component.
|
|
23
|
+
*
|
|
24
|
+
* Fully presentational: renders entirely from props, including the
|
|
25
|
+
* `isCreating` in-flight state and a submit `error`. Standalone
|
|
26
|
+
* consumers (embedded builders, guided tours) can therefore depict
|
|
27
|
+
* any review state — success preview or failure — deterministically.
|
|
23
28
|
*/
|
|
24
29
|
export function ReviewStep({
|
|
25
30
|
org,
|
|
@@ -27,6 +32,17 @@ export function ReviewStep({
|
|
|
27
32
|
isCreating,
|
|
28
33
|
error,
|
|
29
34
|
}: ReviewStepProps) {
|
|
35
|
+
const errorRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
// The error panel renders below the (tall) YAML preview inside the wizard's
|
|
38
|
+
// internal scroll area, so a failed create could otherwise report itself
|
|
39
|
+
// out of view. Bring it into view whenever a new error arrives.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (error) {
|
|
42
|
+
errorRef.current?.scrollIntoView({ block: "nearest" });
|
|
43
|
+
}
|
|
44
|
+
}, [error]);
|
|
45
|
+
|
|
30
46
|
const mcpServerInput = useMemo(
|
|
31
47
|
() => buildMcpServerInput(org, data),
|
|
32
48
|
[org, data],
|
|
@@ -113,6 +129,7 @@ export function ReviewStep({
|
|
|
113
129
|
{/* Error display */}
|
|
114
130
|
{error && (
|
|
115
131
|
<div
|
|
132
|
+
ref={errorRef}
|
|
116
133
|
className="rounded-md border border-destructive bg-muted-faint px-4 py-3"
|
|
117
134
|
role="alert"
|
|
118
135
|
>
|
|
@@ -17,6 +17,8 @@ export interface UseMcpServerCountOptions {
|
|
|
17
17
|
* @default "org"
|
|
18
18
|
*/
|
|
19
19
|
readonly scope?: ResourceListScope;
|
|
20
|
+
/** Opaque token that forces a recount when its value changes. */
|
|
21
|
+
readonly refetchToken?: unknown;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Return value of {@link useMcpServerCount}. */
|
|
@@ -140,6 +140,7 @@ export function CreationPicker<TData>({
|
|
|
140
140
|
description={`Create a blank ${resourceLabel} and configure it step by step.`}
|
|
141
141
|
icon={<BlankPageIcon />}
|
|
142
142
|
onClick={() => onSelect({ kind: "scratch" })}
|
|
143
|
+
cursorTarget="creation-path-scratch"
|
|
143
144
|
/>
|
|
144
145
|
|
|
145
146
|
{/* Browse templates */}
|
|
@@ -150,6 +151,7 @@ export function CreationPicker<TData>({
|
|
|
150
151
|
icon={<GridIcon />}
|
|
151
152
|
onClick={() => setView("gallery")}
|
|
152
153
|
badge={`${templates.length} available`}
|
|
154
|
+
cursorTarget="creation-path-template"
|
|
153
155
|
/>
|
|
154
156
|
)}
|
|
155
157
|
|
|
@@ -159,6 +161,7 @@ export function CreationPicker<TData>({
|
|
|
159
161
|
description="Upload a YAML or JSON configuration file."
|
|
160
162
|
icon={<UploadIcon />}
|
|
161
163
|
onClick={() => onSelect({ kind: "import" })}
|
|
164
|
+
cursorTarget="creation-path-import"
|
|
162
165
|
/>
|
|
163
166
|
</div>
|
|
164
167
|
</div>
|
|
@@ -175,17 +178,21 @@ function OptionCard({
|
|
|
175
178
|
icon,
|
|
176
179
|
onClick,
|
|
177
180
|
badge,
|
|
181
|
+
cursorTarget,
|
|
178
182
|
}: {
|
|
179
183
|
readonly title: string;
|
|
180
184
|
readonly description: string;
|
|
181
185
|
readonly icon: React.ReactNode;
|
|
182
186
|
readonly onClick: () => void;
|
|
183
187
|
readonly badge?: string;
|
|
188
|
+
/** `data-cursor-target` hook for guided tours and demo playbacks. */
|
|
189
|
+
readonly cursorTarget?: string;
|
|
184
190
|
}) {
|
|
185
191
|
return (
|
|
186
192
|
<button
|
|
187
193
|
type="button"
|
|
188
194
|
onClick={onClick}
|
|
195
|
+
data-cursor-target={cursorTarget}
|
|
189
196
|
className={cn(
|
|
190
197
|
"group flex flex-col items-center gap-3 rounded-lg border border-border bg-card p-6 text-center",
|
|
191
198
|
"transition-colors hover:border-primary hover:bg-accent",
|
|
@@ -90,6 +90,7 @@ export function WizardNav({
|
|
|
90
90
|
type="button"
|
|
91
91
|
onClick={onNext}
|
|
92
92
|
disabled={nextDisabled || isSubmitting}
|
|
93
|
+
data-cursor-target="wizard-next"
|
|
93
94
|
className={cn(
|
|
94
95
|
"inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground transition-colors",
|
|
95
96
|
"hover:bg-primary-hover",
|
|
@@ -9,6 +9,13 @@ export interface SectionProps {
|
|
|
9
9
|
/** Arbitrary action elements rendered in the header row (e.g. expand, share). */
|
|
10
10
|
readonly headerActions?: ReactNode;
|
|
11
11
|
readonly onEdit?: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Stable id emitted as `data-scroll-target` on the section element, so
|
|
14
|
+
* guided tours and demos can scroll the section into view by name (the
|
|
15
|
+
* same convention as `data-cursor-target`). Purely an annotation — no
|
|
16
|
+
* behavior attaches to it here.
|
|
17
|
+
*/
|
|
18
|
+
readonly scrollTarget?: string;
|
|
12
19
|
readonly children: ReactNode;
|
|
13
20
|
readonly className?: string;
|
|
14
21
|
}
|
|
@@ -26,11 +33,12 @@ export function Section({
|
|
|
26
33
|
count,
|
|
27
34
|
headerActions,
|
|
28
35
|
onEdit,
|
|
36
|
+
scrollTarget,
|
|
29
37
|
children,
|
|
30
38
|
className,
|
|
31
39
|
}: SectionProps) {
|
|
32
40
|
return (
|
|
33
|
-
<section className={className}>
|
|
41
|
+
<section className={className} data-scroll-target={scrollTarget}>
|
|
34
42
|
<div className="mb-2 flex items-center gap-2">
|
|
35
43
|
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
36
44
|
{title}
|
|
@@ -112,6 +112,17 @@ export interface ResourceWorkbenchProps<TData = SearchResult> {
|
|
|
112
112
|
/** Extracts a unique ID from an item (for selection tracking). */
|
|
113
113
|
readonly getItemId?: (item: TData) => string;
|
|
114
114
|
|
|
115
|
+
// --- Refresh -----------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Opaque token that forces a background refetch of the list whenever
|
|
119
|
+
* its value changes — the current view stays rendered with a refetch
|
|
120
|
+
* shimmer (no remount flash), and pagination and sort are preserved.
|
|
121
|
+
* Bump it after an out-of-band mutation (applying a manifest, deleting
|
|
122
|
+
* a row) so the change appears without a reload.
|
|
123
|
+
*/
|
|
124
|
+
readonly refetchToken?: unknown;
|
|
125
|
+
|
|
115
126
|
// --- Empty & error states ----------------------------------------------
|
|
116
127
|
|
|
117
128
|
/** Icon for the empty state. */
|
|
@@ -210,6 +221,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
210
221
|
onStateChange,
|
|
211
222
|
enableSelection = false,
|
|
212
223
|
getItemId = defaultGetId as (item: TData) => string,
|
|
224
|
+
refetchToken,
|
|
213
225
|
emptyIcon,
|
|
214
226
|
emptyTitle = "No resources found",
|
|
215
227
|
emptyDescription = "Try adjusting your search or filters.",
|
|
@@ -255,6 +267,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
255
267
|
onSortChange: filtersHook.setSort,
|
|
256
268
|
columns: columns as WorkbenchColumnDef<TData>[],
|
|
257
269
|
enableSelection,
|
|
270
|
+
refetchToken,
|
|
258
271
|
});
|
|
259
272
|
|
|
260
273
|
// --- Selection ---------------------------------------------------------
|
|
@@ -57,6 +57,15 @@ export interface UseResourceCollectionOptions<TData = SearchResult> {
|
|
|
57
57
|
readonly columns?: readonly WorkbenchColumnDef<TData>[];
|
|
58
58
|
/** Enable row selection state tracking. @default false */
|
|
59
59
|
readonly enableSelection?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Opaque token that forces a background refetch whenever its value
|
|
62
|
+
* changes. Use it to re-read the list after an out-of-band mutation
|
|
63
|
+
* (e.g. applying a manifest or deleting a row) without remounting:
|
|
64
|
+
* bump a counter and pass it here. The current page stays rendered
|
|
65
|
+
* with an `isRefetching` shimmer — no full-list flash. A change to
|
|
66
|
+
* this token alone never resets pagination or sort.
|
|
67
|
+
*/
|
|
68
|
+
readonly refetchToken?: unknown;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
// ---------------------------------------------------------------------------
|
|
@@ -148,6 +157,7 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
148
157
|
onSortChange,
|
|
149
158
|
columns = [],
|
|
150
159
|
enableSelection = false,
|
|
160
|
+
refetchToken,
|
|
151
161
|
} = options;
|
|
152
162
|
|
|
153
163
|
// --- Data fetching via useFetch -----------------------------------------
|
|
@@ -169,7 +179,7 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
169
179
|
};
|
|
170
180
|
}
|
|
171
181
|
: null,
|
|
172
|
-
[listFn, org, query, scope, page, pageSize],
|
|
182
|
+
[listFn, org, query, scope, page, pageSize, refetchToken],
|
|
173
183
|
INITIAL_DATA,
|
|
174
184
|
);
|
|
175
185
|
|
|
@@ -16,6 +16,13 @@ export interface UseResourceCountOptions {
|
|
|
16
16
|
* @default "org"
|
|
17
17
|
*/
|
|
18
18
|
readonly scope?: ResourceListScope;
|
|
19
|
+
/**
|
|
20
|
+
* Opaque token that forces a recount whenever its value changes. Use
|
|
21
|
+
* it to refresh the count after an out-of-band mutation (e.g. applying
|
|
22
|
+
* a manifest) without remounting. Mirrors the `refetchToken` on
|
|
23
|
+
* {@link useResourceCollection}.
|
|
24
|
+
*/
|
|
25
|
+
readonly refetchToken?: unknown;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface UseResourceCountReturn {
|
|
@@ -52,6 +59,7 @@ export function useResourceCount(
|
|
|
52
59
|
): UseResourceCountReturn {
|
|
53
60
|
const query = options?.query;
|
|
54
61
|
const scope = options?.scope ?? "org";
|
|
62
|
+
const refetchToken = options?.refetchToken;
|
|
55
63
|
|
|
56
64
|
const { data: count, isLoading, isRefetching, error, refetch } = useFetch<number | undefined>(
|
|
57
65
|
org
|
|
@@ -66,7 +74,7 @@ export function useResourceCount(
|
|
|
66
74
|
return result.totalCount;
|
|
67
75
|
}
|
|
68
76
|
: null,
|
|
69
|
-
[listFn, org, query, scope],
|
|
77
|
+
[listFn, org, query, scope, refetchToken],
|
|
70
78
|
undefined,
|
|
71
79
|
);
|
|
72
80
|
|