@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
package/skill/useSkillCount.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface UseSkillCountOptions {
|
|
|
12
12
|
* @default "org"
|
|
13
13
|
*/
|
|
14
14
|
readonly scope?: ResourceListScope;
|
|
15
|
+
/** Opaque token that forces a recount when its value changes. */
|
|
16
|
+
readonly refetchToken?: unknown;
|
|
15
17
|
}
|
|
16
18
|
/** Return value of {@link useSkillCount}. */
|
|
17
19
|
export interface UseSkillCountReturn {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSkillCount.d.ts","sourceRoot":"","sources":["../../src/skill/useSkillCount.ts"],"names":[],"mappings":"AAIA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE9E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"useSkillCount.d.ts","sourceRoot":"","sources":["../../src/skill/useSkillCount.ts"],"names":[],"mappings":"AAIA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE9E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;IACnC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,mBAAmB,CAUrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSkillCount.js","sourceRoot":"","sources":["../../src/skill/useSkillCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAA0B,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"useSkillCount.js","sourceRoot":"","sources":["../../src/skill/useSkillCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAA0B,MAAM,oBAAoB,CAAC;AAkC9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAkB,EAClB,OAA8B;IAE9B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,MAAgD,EAAE,EAAE,CACnD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAC5B,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -17,6 +17,8 @@ export interface UseAgentCountOptions {
|
|
|
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 useAgentCount}. */
|
|
@@ -22,6 +22,12 @@ export interface CreateApiKeyFormProps {
|
|
|
22
22
|
readonly onCreated?: (apiKey: ApiKey) => void;
|
|
23
23
|
/** Fired when the user cancels creation. */
|
|
24
24
|
readonly onCancel?: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Seeds the name field on mount (e.g. a suggested key name, or a demo
|
|
27
|
+
* scenario depicting the form mid-fill). One-time: consumed on mount;
|
|
28
|
+
* subsequent changes are ignored. The field stays fully editable.
|
|
29
|
+
*/
|
|
30
|
+
readonly initialName?: string;
|
|
25
31
|
/** Additional CSS class names for the root container. */
|
|
26
32
|
readonly className?: string;
|
|
27
33
|
}
|
|
@@ -51,11 +57,12 @@ export function CreateApiKeyForm({
|
|
|
51
57
|
org,
|
|
52
58
|
onCreated,
|
|
53
59
|
onCancel,
|
|
60
|
+
initialName = "",
|
|
54
61
|
className,
|
|
55
62
|
}: CreateApiKeyFormProps) {
|
|
56
63
|
const { create, isCreating, error, clearError } = useCreateApiKey();
|
|
57
64
|
|
|
58
|
-
const [name, setName] = useState(
|
|
65
|
+
const [name, setName] = useState(initialName);
|
|
59
66
|
const [expiry, setExpiry] = useState<ExpiryOption>("never");
|
|
60
67
|
|
|
61
68
|
const trimmedName = name.trim();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { CreateApiKeyForm } from "../CreateApiKeyForm";
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
function createWrapper() {
|
|
11
|
+
const mockClient = {
|
|
12
|
+
apiKey: { create: vi.fn() },
|
|
13
|
+
} as unknown as Stigmer;
|
|
14
|
+
|
|
15
|
+
return ({ children }: { children: ReactNode }) => (
|
|
16
|
+
<StigmerContext.Provider value={mockClient}>
|
|
17
|
+
{children}
|
|
18
|
+
</StigmerContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nameInput(): HTMLInputElement {
|
|
23
|
+
return screen.getByLabelText("Name") as HTMLInputElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function submitButton(): HTMLButtonElement {
|
|
27
|
+
return screen.getByRole("button", {
|
|
28
|
+
name: "Create API key",
|
|
29
|
+
}) as HTMLButtonElement;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("CreateApiKeyForm initialName", () => {
|
|
33
|
+
it("starts with an empty name and a disabled submit by default", () => {
|
|
34
|
+
render(<CreateApiKeyForm org="acme" />, { wrapper: createWrapper() });
|
|
35
|
+
|
|
36
|
+
expect(nameInput().value).toBe("");
|
|
37
|
+
expect(submitButton().disabled).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("seeds the name field and enables submit when initialName is set", () => {
|
|
41
|
+
render(<CreateApiKeyForm org="acme" initialName="quickstart-key" />, {
|
|
42
|
+
wrapper: createWrapper(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(nameInput().value).toBe("quickstart-key");
|
|
46
|
+
expect(submitButton().disabled).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the seeded field editable — initialName is a seed, not a lock", () => {
|
|
50
|
+
render(<CreateApiKeyForm org="acme" initialName="quickstart-key" />, {
|
|
51
|
+
wrapper: createWrapper(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
fireEvent.change(nameInput(), { target: { value: "renamed-key" } });
|
|
55
|
+
|
|
56
|
+
expect(nameInput().value).toBe("renamed-key");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -17,6 +17,8 @@ export interface UseDatastoreCountOptions {
|
|
|
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 useDatastoreCount}. */
|
package/src/index.ts
CHANGED
|
@@ -156,7 +156,9 @@ export {
|
|
|
156
156
|
channelSessionExternalUserKey,
|
|
157
157
|
SessionViewer,
|
|
158
158
|
NewSessionViewer,
|
|
159
|
+
SessionViewerLayout,
|
|
159
160
|
useSessionPanel,
|
|
161
|
+
useSessionRailViews,
|
|
160
162
|
usePlanDraft,
|
|
161
163
|
planDraftKey,
|
|
162
164
|
PlanEditor,
|
|
@@ -201,8 +203,10 @@ export type {
|
|
|
201
203
|
SearchResultGroup,
|
|
202
204
|
SessionViewerProps,
|
|
203
205
|
NewSessionViewerProps,
|
|
206
|
+
SessionViewerLayoutProps,
|
|
204
207
|
SessionPanelController,
|
|
205
208
|
UseSessionPanelOptions,
|
|
209
|
+
UseSessionRailViewsOptions,
|
|
206
210
|
PlanDraftController,
|
|
207
211
|
PlanEditorProps,
|
|
208
212
|
PlanStreamingDocumentProps,
|
|
@@ -526,6 +530,10 @@ export {
|
|
|
526
530
|
useUpdateMcpServer,
|
|
527
531
|
mcpServerToInput,
|
|
528
532
|
toServerKey,
|
|
533
|
+
IdentityTransportStep,
|
|
534
|
+
EnvironmentAuthStep,
|
|
535
|
+
ReviewStep,
|
|
536
|
+
createInitialMcpServerWizardData,
|
|
529
537
|
} from "./mcp-server/index.js";
|
|
530
538
|
export type {
|
|
531
539
|
UseMcpServerReturn,
|
|
@@ -563,6 +571,9 @@ export type {
|
|
|
563
571
|
McpServerCreationWizardProps,
|
|
564
572
|
McpServerCreationResult,
|
|
565
573
|
McpServerWizardData,
|
|
574
|
+
IdentityTransportStepProps,
|
|
575
|
+
EnvironmentAuthStepProps,
|
|
576
|
+
ReviewStepProps,
|
|
566
577
|
} from "./mcp-server/index.js";
|
|
567
578
|
|
|
568
579
|
// Skill — data hooks, upload, file browser, and mutation
|
|
@@ -23,10 +23,15 @@ export interface ApplyManifestDialogProps {
|
|
|
23
23
|
/** Target organization slug for documents that omit `metadata.org`. */
|
|
24
24
|
readonly org: string;
|
|
25
25
|
/**
|
|
26
|
-
* Called
|
|
27
|
-
*
|
|
26
|
+
* Called when the dialog closes after at least one document applied,
|
|
27
|
+
* with the applied entries in apply order — on full success, and also
|
|
28
|
+
* when the operator dismisses the dialog after a partial apply (some
|
|
29
|
+
* documents applied, a later one failed). Use it to refresh a list so
|
|
30
|
+
* the newly applied resources appear without a reload. It never fires
|
|
31
|
+
* when nothing applied, and fires once per close. Mirrors
|
|
32
|
+
* {@link EditResourceYamlDialog}'s `onApplied`.
|
|
28
33
|
*/
|
|
29
|
-
readonly
|
|
34
|
+
readonly onApplied?: (applied: readonly ManifestPreviewEntry[]) => void;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -56,7 +61,7 @@ export interface ApplyManifestDialogProps {
|
|
|
56
61
|
* open={open}
|
|
57
62
|
* onOpenChange={setOpen}
|
|
58
63
|
* org={activeOrg}
|
|
59
|
-
*
|
|
64
|
+
* onApplied={() => refetchList()}
|
|
60
65
|
* />
|
|
61
66
|
* ```
|
|
62
67
|
*/
|
|
@@ -64,7 +69,7 @@ export function ApplyManifestDialog({
|
|
|
64
69
|
open,
|
|
65
70
|
onOpenChange,
|
|
66
71
|
org,
|
|
67
|
-
|
|
72
|
+
onApplied,
|
|
68
73
|
}: ApplyManifestDialogProps) {
|
|
69
74
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
70
75
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -90,12 +95,26 @@ export function ApplyManifestDialog({
|
|
|
90
95
|
}
|
|
91
96
|
}, [open, reset]);
|
|
92
97
|
|
|
98
|
+
// Close the dialog and, if any document applied this session, refresh
|
|
99
|
+
// the caller's list. This is the dismiss-after-partial path: some
|
|
100
|
+
// documents applied before a later one failed, and the operator closes
|
|
101
|
+
// the dialog rather than retrying — those applied resources must still
|
|
102
|
+
// appear. The full-success path in `handleApply` fires `onApplied`
|
|
103
|
+
// itself (it holds the fresh applied list), so it never routes here.
|
|
104
|
+
const closeAndMaybeRefresh = useCallback(() => {
|
|
105
|
+
onOpenChange(false);
|
|
106
|
+
const applied = (manifest.entries ?? []).filter(
|
|
107
|
+
(e) => e.status === "applied",
|
|
108
|
+
);
|
|
109
|
+
if (applied.length > 0) onApplied?.(applied);
|
|
110
|
+
}, [onOpenChange, manifest.entries, onApplied]);
|
|
111
|
+
|
|
93
112
|
const handleCancel = useCallback(
|
|
94
113
|
(e: React.SyntheticEvent) => {
|
|
95
114
|
e.preventDefault();
|
|
96
|
-
|
|
115
|
+
closeAndMaybeRefresh();
|
|
97
116
|
},
|
|
98
|
-
[
|
|
117
|
+
[closeAndMaybeRefresh],
|
|
99
118
|
);
|
|
100
119
|
|
|
101
120
|
const handleFileChange = useCallback(
|
|
@@ -107,18 +126,23 @@ export function ApplyManifestDialog({
|
|
|
107
126
|
);
|
|
108
127
|
|
|
109
128
|
const handleApply = useCallback(async () => {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
const results = await manifest.applyAll();
|
|
130
|
+
const applied = results.filter((e) => e.status === "applied");
|
|
131
|
+
const allApplied = results.length > 0 && applied.length === results.length;
|
|
132
|
+
if (allApplied) {
|
|
113
133
|
toast.success(
|
|
114
|
-
|
|
134
|
+
applied.length === 1
|
|
135
|
+
? "Resource applied"
|
|
136
|
+
: `${applied.length} resources applied`,
|
|
115
137
|
);
|
|
116
138
|
onOpenChange(false);
|
|
117
|
-
|
|
139
|
+
onApplied?.(applied);
|
|
118
140
|
}
|
|
119
|
-
// On failure the dialog stays open — per-entry status
|
|
120
|
-
// applied and what failed.
|
|
121
|
-
|
|
141
|
+
// On partial/total failure the dialog stays open — per-entry status
|
|
142
|
+
// shows what applied and what failed. Any entries that DID apply
|
|
143
|
+
// still refresh the caller's list when the dialog is dismissed
|
|
144
|
+
// (closeAndMaybeRefresh), so successful writes are never hidden.
|
|
145
|
+
}, [manifest, onOpenChange, onApplied]);
|
|
122
146
|
|
|
123
147
|
const canApply =
|
|
124
148
|
manifest.entries !== null &&
|
|
@@ -198,7 +222,7 @@ export function ApplyManifestDialog({
|
|
|
198
222
|
<div className="flex gap-2">
|
|
199
223
|
<button
|
|
200
224
|
type="button"
|
|
201
|
-
onClick={
|
|
225
|
+
onClick={closeAndMaybeRefresh}
|
|
202
226
|
disabled={manifest.isApplying}
|
|
203
227
|
className={cn(
|
|
204
228
|
"rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Tests for ApplyManifestDialog's onApplied contract — the refresh
|
|
2
|
+
// signal every Library list page wires to appear-without-reload. It must
|
|
3
|
+
// fire on full success AND when the operator dismisses the dialog after a
|
|
4
|
+
// partial apply (some documents applied, a later one failed), and must
|
|
5
|
+
// never fire when nothing applied. Historically onApplied's predecessor
|
|
6
|
+
// had zero coverage and stayed unwired at every call site; this locks the
|
|
7
|
+
// contract the list pages now depend on.
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
10
|
+
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
11
|
+
import { ApplyManifestDialog } from "../ApplyManifestDialog";
|
|
12
|
+
import type {
|
|
13
|
+
ManifestPreviewEntry,
|
|
14
|
+
UseApplyManifestReturn,
|
|
15
|
+
} from "../useApplyManifest";
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
19
|
+
this.open = true;
|
|
20
|
+
};
|
|
21
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
22
|
+
this.open = false;
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
cleanup();
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// A preview entry in a chosen terminal status; the document shape is only
|
|
32
|
+
// what the dialog's preview row and apply flow read.
|
|
33
|
+
function entry(
|
|
34
|
+
slug: string,
|
|
35
|
+
status: ManifestPreviewEntry["status"],
|
|
36
|
+
): ManifestPreviewEntry {
|
|
37
|
+
return {
|
|
38
|
+
document: {
|
|
39
|
+
handler: { yamlKind: "datastore", displayName: "Datastore" },
|
|
40
|
+
name: slug,
|
|
41
|
+
slug,
|
|
42
|
+
org: "acme",
|
|
43
|
+
},
|
|
44
|
+
action: "create",
|
|
45
|
+
status,
|
|
46
|
+
} as ManifestPreviewEntry;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The mock's mutable knobs: the entries the hook exposes and the terminal
|
|
50
|
+
// entries applyAll resolves to. Set per test before rendering.
|
|
51
|
+
let mockEntries: readonly ManifestPreviewEntry[] | null;
|
|
52
|
+
let applyAllResult: readonly ManifestPreviewEntry[];
|
|
53
|
+
const applyAllMock = vi.fn(async () => applyAllResult);
|
|
54
|
+
|
|
55
|
+
vi.mock("../useApplyManifest", () => ({
|
|
56
|
+
useApplyManifest: (): UseApplyManifestReturn =>
|
|
57
|
+
({
|
|
58
|
+
content: "kind: Datastore",
|
|
59
|
+
setContent: vi.fn(),
|
|
60
|
+
readFile: vi.fn(),
|
|
61
|
+
entries: mockEntries,
|
|
62
|
+
validationError: null,
|
|
63
|
+
isValidating: false,
|
|
64
|
+
hasRedactedSecrets: false,
|
|
65
|
+
applyAll: applyAllMock,
|
|
66
|
+
isApplying: false,
|
|
67
|
+
reset: vi.fn(),
|
|
68
|
+
}) as UseApplyManifestReturn,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// CodeMirror stays out of the test environment.
|
|
72
|
+
vi.mock("../YamlEditor", () => ({
|
|
73
|
+
YamlEditor: () => <div data-testid="yaml-editor" />,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
describe("ApplyManifestDialog — onApplied", () => {
|
|
77
|
+
it("fires with the applied entries on full success and closes", async () => {
|
|
78
|
+
mockEntries = [entry("a", "pending"), entry("b", "pending")];
|
|
79
|
+
applyAllResult = [entry("a", "applied"), entry("b", "applied")];
|
|
80
|
+
const onApplied = vi.fn();
|
|
81
|
+
const onOpenChange = vi.fn();
|
|
82
|
+
|
|
83
|
+
render(
|
|
84
|
+
<ApplyManifestDialog
|
|
85
|
+
open
|
|
86
|
+
onOpenChange={onOpenChange}
|
|
87
|
+
org="acme"
|
|
88
|
+
onApplied={onApplied}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
fireEvent.click(screen.getByRole("button", { name: /apply/i }));
|
|
93
|
+
|
|
94
|
+
await waitFor(() => expect(onApplied).toHaveBeenCalledTimes(1));
|
|
95
|
+
expect(
|
|
96
|
+
onApplied.mock.calls[0][0].map((e: ManifestPreviewEntry) => e.document.slug),
|
|
97
|
+
).toEqual(["a", "b"]);
|
|
98
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("fires with only the applied subset when a partial apply is dismissed", async () => {
|
|
102
|
+
// Applied one, failed the next — the dialog stays open on failure.
|
|
103
|
+
mockEntries = [entry("a", "applied"), entry("b", "failed")];
|
|
104
|
+
applyAllResult = [entry("a", "applied"), entry("b", "failed")];
|
|
105
|
+
const onApplied = vi.fn();
|
|
106
|
+
const onOpenChange = vi.fn();
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<ApplyManifestDialog
|
|
110
|
+
open
|
|
111
|
+
onOpenChange={onOpenChange}
|
|
112
|
+
org="acme"
|
|
113
|
+
onApplied={onApplied}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Apply first: full success did not happen, so onApplied has not fired.
|
|
118
|
+
fireEvent.click(screen.getByRole("button", { name: /apply/i }));
|
|
119
|
+
await waitFor(() => expect(applyAllMock).toHaveBeenCalled());
|
|
120
|
+
expect(onApplied).not.toHaveBeenCalled();
|
|
121
|
+
|
|
122
|
+
// Dismiss with an applied entry present → refresh the list.
|
|
123
|
+
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
124
|
+
expect(onApplied).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(
|
|
126
|
+
onApplied.mock.calls[0][0].map((e: ManifestPreviewEntry) => e.document.slug),
|
|
127
|
+
).toEqual(["a"]);
|
|
128
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not fire when the dialog is dismissed with nothing applied", () => {
|
|
132
|
+
mockEntries = [entry("a", "pending")];
|
|
133
|
+
const onApplied = vi.fn();
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<ApplyManifestDialog
|
|
137
|
+
open
|
|
138
|
+
onOpenChange={vi.fn()}
|
|
139
|
+
org="acme"
|
|
140
|
+
onApplied={onApplied}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
145
|
+
expect(onApplied).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -174,12 +174,12 @@ describe("useApplyManifest", () => {
|
|
|
174
174
|
});
|
|
175
175
|
await act(settleValidation);
|
|
176
176
|
|
|
177
|
-
let
|
|
177
|
+
let applied: readonly { status: string }[] = [];
|
|
178
178
|
await act(async () => {
|
|
179
|
-
|
|
179
|
+
applied = await result.current.applyAll();
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
expect(
|
|
182
|
+
expect(applied.map((e) => e.status)).toEqual(["applied", "applied"]);
|
|
183
183
|
expect(apply).toHaveBeenCalledTimes(2);
|
|
184
184
|
expect(result.current.entries!.map((e) => e.status)).toEqual([
|
|
185
185
|
"applied",
|
|
@@ -201,12 +201,12 @@ describe("useApplyManifest", () => {
|
|
|
201
201
|
});
|
|
202
202
|
await act(settleValidation);
|
|
203
203
|
|
|
204
|
-
let
|
|
204
|
+
let applied: readonly { status: string }[] = [];
|
|
205
205
|
await act(async () => {
|
|
206
|
-
|
|
206
|
+
applied = await result.current.applyAll();
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
expect(
|
|
209
|
+
expect(applied.map((e) => e.status)).toEqual(["failed", "skipped"]);
|
|
210
210
|
expect(apply).toHaveBeenCalledTimes(1);
|
|
211
211
|
expect(result.current.entries!.map((e) => e.status)).toEqual([
|
|
212
212
|
"failed",
|
|
@@ -55,9 +55,13 @@ export interface UseApplyManifestReturn {
|
|
|
55
55
|
/**
|
|
56
56
|
* Apply all documents sequentially in dependency order. Entries update
|
|
57
57
|
* live. Stops at the first failure (later entries become `"skipped"`).
|
|
58
|
-
* Resolves
|
|
58
|
+
* Resolves to the final per-document entries with terminal statuses —
|
|
59
|
+
* the caller derives full success (`every applied`) and the applied
|
|
60
|
+
* subset (for a list refresh) without re-reading React state, which is
|
|
61
|
+
* still stale in the calling closure. Empty when there was nothing to
|
|
62
|
+
* apply.
|
|
59
63
|
*/
|
|
60
|
-
readonly applyAll: () => Promise<
|
|
64
|
+
readonly applyAll: () => Promise<readonly ManifestPreviewEntry[]>;
|
|
61
65
|
/** `true` while applies are in flight. */
|
|
62
66
|
readonly isApplying: boolean;
|
|
63
67
|
/** Clear all state (content, preview, errors). */
|
|
@@ -194,8 +198,8 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
|
|
|
194
198
|
return () => clearTimeout(timer);
|
|
195
199
|
}, [content, stigmer, isApplying]);
|
|
196
200
|
|
|
197
|
-
const applyAll = useCallback(async (): Promise<
|
|
198
|
-
if (!entries || entries.length === 0) return
|
|
201
|
+
const applyAll = useCallback(async (): Promise<readonly ManifestPreviewEntry[]> => {
|
|
202
|
+
if (!entries || entries.length === 0) return [];
|
|
199
203
|
|
|
200
204
|
// Invalidate any in-flight validation so it cannot clobber apply statuses.
|
|
201
205
|
validateSeq.current++;
|
|
@@ -206,10 +210,10 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
|
|
|
206
210
|
const working = entries.map((entry): ManifestPreviewEntry => ({ ...entry, status: "pending" }));
|
|
207
211
|
const commit = () => setEntries([...working]);
|
|
208
212
|
|
|
209
|
-
let
|
|
213
|
+
let stopped = false;
|
|
210
214
|
|
|
211
215
|
for (let i = 0; i < working.length; i++) {
|
|
212
|
-
if (
|
|
216
|
+
if (stopped) {
|
|
213
217
|
working[i] = { ...working[i], status: "skipped" };
|
|
214
218
|
continue;
|
|
215
219
|
}
|
|
@@ -226,14 +230,14 @@ export function useApplyManifest(org: string): UseApplyManifestReturn {
|
|
|
226
230
|
status: "failed",
|
|
227
231
|
errorMessage: toError(err).message,
|
|
228
232
|
};
|
|
229
|
-
|
|
233
|
+
stopped = true;
|
|
230
234
|
}
|
|
231
235
|
commit();
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
commit();
|
|
235
239
|
setIsApplying(false);
|
|
236
|
-
return
|
|
240
|
+
return working;
|
|
237
241
|
}, [entries, stigmer]);
|
|
238
242
|
|
|
239
243
|
const hasRedactedSecrets = useMemo(
|
|
@@ -14,6 +14,7 @@ import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigm
|
|
|
14
14
|
import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
15
15
|
import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
16
16
|
import { useMcpServer } from "./useMcpServer.js";
|
|
17
|
+
import type { UseMcpServerReturn } from "./useMcpServer.js";
|
|
17
18
|
import { useUpdateMcpServer } from "./useUpdateMcpServer.js";
|
|
18
19
|
import { mcpServerToInput } from "./internal/mcpServerToInput.js";
|
|
19
20
|
import { useMcpServerConnect } from "./useMcpServerConnect.js";
|
|
@@ -51,6 +52,31 @@ export interface McpServerDetailViewProps {
|
|
|
51
52
|
readonly org: string;
|
|
52
53
|
/** MCP server slug (URL-friendly identifier unique within the org). */
|
|
53
54
|
readonly slug: string;
|
|
55
|
+
/**
|
|
56
|
+
* Hoisted resource state from {@link useMcpServer}, for callers that
|
|
57
|
+
* already own the fetch. When provided, the view issues **no**
|
|
58
|
+
* `getByReference` RPC of its own and renders entirely from this state —
|
|
59
|
+
* loading, error, and not-found included, so a caller mid-fetch still
|
|
60
|
+
* gets the loading skeleton rather than a false not-found.
|
|
61
|
+
*
|
|
62
|
+
* Ownership contract: supplying this state means supplying its
|
|
63
|
+
* lifecycle. The view calls `mcpServerState.refetch()` after connect,
|
|
64
|
+
* disconnect, and inline edits, so the `refetch` you pass must refresh
|
|
65
|
+
* the data behind `mcpServer` or the view will render stale state after
|
|
66
|
+
* those actions.
|
|
67
|
+
*
|
|
68
|
+
* Two callers this serves:
|
|
69
|
+
* - A page that already fetched the server (e.g. for export/YAML
|
|
70
|
+
* actions) passes `useMcpServer(org, slug)` straight through instead
|
|
71
|
+
* of paying a duplicate RPC.
|
|
72
|
+
* - A guided tour or demo supplies frozen state
|
|
73
|
+
* (`{ mcpServer, isLoading: false, isRefetching: false, error: null,
|
|
74
|
+
* refetch: noop }`) so every depicted beat renders deterministically
|
|
75
|
+
* from data the tour owns (scenar-cloud DD-006).
|
|
76
|
+
*
|
|
77
|
+
* When omitted, the view fetches by `org`/`slug` itself, as before.
|
|
78
|
+
*/
|
|
79
|
+
readonly mcpServerState?: UseMcpServerReturn;
|
|
54
80
|
/**
|
|
55
81
|
* Called once when the MCP server resource has been fetched successfully.
|
|
56
82
|
* Provides the resource display name for use cases like breadcrumbs,
|
|
@@ -115,7 +141,8 @@ export interface McpServerDetailViewProps {
|
|
|
115
141
|
/**
|
|
116
142
|
* Detail view for an MCP Server integration.
|
|
117
143
|
*
|
|
118
|
-
* Fetches the server via {@link useMcpServer} internally
|
|
144
|
+
* Fetches the server via {@link useMcpServer} internally — or renders
|
|
145
|
+
* from hoisted state when the caller passes `mcpServerState` — and shows
|
|
119
146
|
* its full configuration: validation banner (if invalid), header,
|
|
120
147
|
* a **Connect bar** with a single Connect/Reconnect action, and a
|
|
121
148
|
* **tabbed capabilities panel** (Tools, Policies, and optionally
|
|
@@ -135,10 +162,20 @@ export interface McpServerDetailViewProps {
|
|
|
135
162
|
* // Minimal — self-contained, fetches its own data
|
|
136
163
|
* <McpServerDetailView org="acme" slug="github" />
|
|
137
164
|
* ```
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```tsx
|
|
168
|
+
* // Hoisted — the page already fetched the server (e.g. for YAML
|
|
169
|
+
* // export actions), so pass that state through instead of paying a
|
|
170
|
+
* // second getByReference RPC.
|
|
171
|
+
* const state = useMcpServer(org, slug);
|
|
172
|
+
* <McpServerDetailView org={org} slug={slug} mcpServerState={state} />
|
|
173
|
+
* ```
|
|
138
174
|
*/
|
|
139
175
|
export function McpServerDetailView({
|
|
140
176
|
org,
|
|
141
177
|
slug,
|
|
178
|
+
mcpServerState,
|
|
142
179
|
onResourceLoad,
|
|
143
180
|
defaultCapabilityTab = "tools",
|
|
144
181
|
defaultShowCredentialForm = false,
|
|
@@ -150,7 +187,16 @@ export function McpServerDetailView({
|
|
|
150
187
|
onResourceUpdated,
|
|
151
188
|
className,
|
|
152
189
|
}: McpServerDetailViewProps) {
|
|
153
|
-
|
|
190
|
+
// Hoisted-state mode: when the caller supplies `mcpServerState`, disable
|
|
191
|
+
// the internal fetch via the hook's documented null-skip (a stable no-op,
|
|
192
|
+
// so `fetched` reports isLoading=false with no RPC) and render from the
|
|
193
|
+
// caller's state instead — all four states (loading/error/not-found/data)
|
|
194
|
+
// transfer intact.
|
|
195
|
+
const fetched = useMcpServer(
|
|
196
|
+
mcpServerState ? null : org,
|
|
197
|
+
mcpServerState ? null : slug,
|
|
198
|
+
);
|
|
199
|
+
const { mcpServer, isLoading, error, refetch } = mcpServerState ?? fetched;
|
|
154
200
|
const { update: updateMcpServer, isUpdating } = useUpdateMcpServer();
|
|
155
201
|
|
|
156
202
|
const saveMcpField = useCallback(
|
|
@@ -493,7 +539,9 @@ export function McpServerDetailView({
|
|
|
493
539
|
/>
|
|
494
540
|
)}
|
|
495
541
|
|
|
496
|
-
|
|
542
|
+
{/* scrollTarget: guided tours/demos bring the connect flow into view
|
|
543
|
+
(see IdentityTransportStep's "mcp-transport" for the convention). */}
|
|
544
|
+
<Section title="Connection" scrollTarget="mcp-connection">
|
|
497
545
|
<StdioSandboxNotice serverType={spec?.serverType} className="mb-3" />
|
|
498
546
|
<OAuthRequiredNotice oauthOnly={spec?.auth?.oauthOnly} className="mb-3" />
|
|
499
547
|
<ConnectBar
|
|
@@ -582,7 +630,7 @@ export function McpServerDetailView({
|
|
|
582
630
|
/>
|
|
583
631
|
</dialog>
|
|
584
632
|
|
|
585
|
-
<Section title="Capabilities">
|
|
633
|
+
<Section title="Capabilities" scrollTarget="mcp-capabilities">
|
|
586
634
|
<Tabs
|
|
587
635
|
tabs={capabilityTabs}
|
|
588
636
|
activeTab={capabilityTab}
|