@stigmer/react 3.4.0 → 3.5.0
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/AgentDetailView.d.ts +14 -4
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +73 -11
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/internal/agentToInput.d.ts +3 -0
- package/agent/internal/agentToInput.d.ts.map +1 -1
- package/agent/internal/agentToInput.js +21 -1
- package/agent/internal/agentToInput.js.map +1 -1
- 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/ApiKeyListPanel.d.ts +8 -1
- package/api-key/ApiKeyListPanel.d.ts.map +1 -1
- package/api-key/ApiKeyListPanel.js +9 -7
- package/api-key/ApiKeyListPanel.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/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
- package/dependency-graph/DependencyTreeNode.js +8 -0
- package/dependency-graph/DependencyTreeNode.js.map +1 -1
- package/dependency-graph/types.d.ts +20 -8
- package/dependency-graph/types.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.js +21 -1
- package/dependency-graph/useDependencyGraph.js.map +1 -1
- package/index.d.ts +6 -4
- package/index.d.ts.map +1 -1
- package/index.js +6 -2
- package/index.js.map +1 -1
- package/internal/scroll-area.d.ts +4 -0
- package/internal/scroll-area.d.ts.map +1 -0
- package/internal/scroll-area.js +15 -0
- package/internal/scroll-area.js.map +1 -0
- package/internal/tooltip.d.ts +7 -0
- package/internal/tooltip.d.ts.map +1 -0
- package/internal/tooltip.js +31 -0
- package/internal/tooltip.js.map +1 -0
- 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/StdioSandboxNotice.d.ts +12 -11
- package/mcp-server/StdioSandboxNotice.d.ts.map +1 -1
- package/mcp-server/StdioSandboxNotice.js +13 -12
- package/mcp-server/StdioSandboxNotice.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-creation/templates/mcp-server-templates.d.ts.map +1 -1
- package/resource-creation/templates/mcp-server-templates.js +17 -14
- package/resource-creation/templates/mcp-server-templates.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/sidebar/SettingsSidebar.d.ts +62 -0
- package/sidebar/SettingsSidebar.d.ts.map +1 -0
- package/sidebar/SettingsSidebar.js +60 -0
- package/sidebar/SettingsSidebar.js.map +1 -0
- package/sidebar/WorkspaceSidebar.d.ts +87 -0
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -0
- package/sidebar/WorkspaceSidebar.js +95 -0
- package/sidebar/WorkspaceSidebar.js.map +1 -0
- package/sidebar/chrome.d.ts +48 -0
- package/sidebar/chrome.d.ts.map +1 -0
- package/sidebar/chrome.js +32 -0
- package/sidebar/chrome.js.map +1 -0
- package/sidebar/index.d.ts +6 -0
- package/sidebar/index.d.ts.map +1 -0
- package/sidebar/index.js +9 -0
- package/sidebar/index.js.map +1 -0
- package/sidebar/types.d.ts +54 -0
- package/sidebar/types.d.ts.map +1 -0
- package/sidebar/types.js +2 -0
- package/sidebar/types.js.map +1 -0
- 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/AgentDetailView.tsx +198 -11
- package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +166 -0
- package/src/agent/internal/__tests__/agentToInput.test.ts +154 -0
- package/src/agent/internal/agentToInput.ts +25 -7
- package/src/agent/useAgentCount.ts +2 -0
- package/src/api-key/ApiKeyListPanel.tsx +17 -4
- 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/dependency-graph/DependencyTreeNode.tsx +8 -0
- package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +108 -0
- package/src/dependency-graph/types.ts +20 -8
- package/src/dependency-graph/useDependencyGraph.ts +23 -1
- package/src/index.ts +24 -0
- package/src/internal/scroll-area.tsx +37 -0
- package/src/internal/tooltip.tsx +66 -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/StdioSandboxNotice.tsx +16 -14
- package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
- package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +3 -2
- package/src/mcp-server/index.ts +13 -0
- package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
- package/src/mcp-server/steps/IdentityTransportStep.tsx +16 -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-creation/templates/mcp-server-templates.ts +17 -14
- 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/sidebar/SettingsSidebar.tsx +173 -0
- package/src/sidebar/WorkspaceSidebar.tsx +386 -0
- package/src/sidebar/__tests__/SettingsSidebar.test.tsx +107 -0
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +236 -0
- package/src/sidebar/chrome.tsx +129 -0
- package/src/sidebar/index.ts +16 -0
- package/src/sidebar/types.ts +55 -0
- package/src/skill/useSkillCount.ts +2 -0
- package/src/test/__tests__/samples.test.ts +161 -2
- package/src/test/index.ts +1 -1
- package/src/test/samples.ts +157 -14
- package/src/workflow/useWorkflowCount.ts +2 -0
- package/styles.css +1 -1
- 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 +100 -7
- package/test/samples.d.ts.map +1 -1
- package/test/samples.js +133 -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,107 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { createRouterTransport } from "@connectrpc/connect";
|
|
5
|
+
import { Stigmer } from "@stigmer/sdk";
|
|
6
|
+
import { StigmerContext } from "../../context";
|
|
7
|
+
import { OrgProvider } from "../../organization/OrgProvider";
|
|
8
|
+
import { SETTINGS_NAV_GROUPS } from "../../settings/settings-nav";
|
|
9
|
+
import { SettingsSidebar } from "../SettingsSidebar";
|
|
10
|
+
import type { RenderSidebarLink } from "../types";
|
|
11
|
+
|
|
12
|
+
vi.mock("../../portal-container", () => ({
|
|
13
|
+
useStigmerPortalContainer: () => document.body,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
18
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
19
|
+
class {
|
|
20
|
+
observe() {}
|
|
21
|
+
unobserve() {}
|
|
22
|
+
disconnect() {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(cleanup);
|
|
28
|
+
|
|
29
|
+
const renderAnchor: RenderSidebarLink = ({
|
|
30
|
+
id,
|
|
31
|
+
href,
|
|
32
|
+
className,
|
|
33
|
+
children,
|
|
34
|
+
"aria-current": ariaCurrent,
|
|
35
|
+
}) => (
|
|
36
|
+
<a href={href} data-row-id={id} aria-current={ariaCurrent} className={className}>
|
|
37
|
+
{children}
|
|
38
|
+
</a>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function renderSidebar(ui: ReactNode) {
|
|
42
|
+
// No RPCs registered: the org switcher degrades gracefully, which is not
|
|
43
|
+
// this suite's subject (covered by WorkspaceSidebar's transport test).
|
|
44
|
+
const client = new Stigmer({
|
|
45
|
+
baseUrl: "/",
|
|
46
|
+
getAccessToken: () => "test-token",
|
|
47
|
+
customTransport: createRouterTransport(() => {}),
|
|
48
|
+
});
|
|
49
|
+
return render(
|
|
50
|
+
<StigmerContext.Provider value={client}>
|
|
51
|
+
<OrgProvider>{ui}</OrgProvider>
|
|
52
|
+
</StigmerContext.Provider>,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function baseProps() {
|
|
57
|
+
return {
|
|
58
|
+
groups: SETTINGS_NAV_GROUPS,
|
|
59
|
+
renderLink: renderAnchor,
|
|
60
|
+
footer: <span data-testid="footer-slot">footer</span>,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("SettingsSidebar", () => {
|
|
65
|
+
it("renders every group heading and item from the nav model", () => {
|
|
66
|
+
const { container } = renderSidebar(<SettingsSidebar {...baseProps()} />);
|
|
67
|
+
|
|
68
|
+
for (const group of SETTINGS_NAV_GROUPS) {
|
|
69
|
+
expect(screen.getByText(group.label)).toBeTruthy();
|
|
70
|
+
for (const item of group.items) {
|
|
71
|
+
const basename = item.href.slice(item.href.lastIndexOf("/") + 1);
|
|
72
|
+
const row = container.querySelector(`[data-row-id="${basename}"]`);
|
|
73
|
+
expect(row?.getAttribute("href")).toBe(item.href);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("matches the active row by exact path or subpath, never by sibling prefix", () => {
|
|
79
|
+
const { container } = renderSidebar(
|
|
80
|
+
<SettingsSidebar {...baseProps()} activePath="/settings/api-keys/rotate" />,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const active = container.querySelector('[data-row-id="api-keys"]')!;
|
|
84
|
+
expect(active.getAttribute("aria-current")).toBe("page");
|
|
85
|
+
expect(active.className).toContain("bg-sidebar-accent");
|
|
86
|
+
|
|
87
|
+
// "/settings/api-keys/rotate" must not light up e.g. "/settings/members".
|
|
88
|
+
const inactive = container.querySelector('[data-row-id="members"]')!;
|
|
89
|
+
expect(inactive.getAttribute("aria-current")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders Back to Sessions as a muted exit row targeting backHref", () => {
|
|
93
|
+
const { container } = renderSidebar(
|
|
94
|
+
<SettingsSidebar {...baseProps()} backHref="/sessions/ses_123" />,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const back = container.querySelector('[data-row-id="back-to-sessions"]')!;
|
|
98
|
+
expect(back.getAttribute("href")).toBe("/sessions/ses_123");
|
|
99
|
+
expect(back.className).toContain("text-sidebar-muted-foreground");
|
|
100
|
+
expect(back.textContent).toContain("Back to Sessions");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders the footer slot", () => {
|
|
104
|
+
renderSidebar(<SettingsSidebar {...baseProps()} />);
|
|
105
|
+
expect(screen.getByTestId("footer-slot")).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, within } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { createRouterTransport } from "@connectrpc/connect";
|
|
6
|
+
import { Stigmer } from "@stigmer/sdk";
|
|
7
|
+
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
8
|
+
import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
|
|
9
|
+
import { OrganizationsSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/io_pb";
|
|
10
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
11
|
+
import { StigmerContext } from "../../context";
|
|
12
|
+
import { OrgProvider } from "../../organization/OrgProvider";
|
|
13
|
+
import { WorkspaceSidebar } from "../WorkspaceSidebar";
|
|
14
|
+
import type { RecentActivityEntry } from "../../activity/types";
|
|
15
|
+
import type { RenderSidebarLink } from "../types";
|
|
16
|
+
|
|
17
|
+
// Tooltips and the org switcher menu portal their content; without a
|
|
18
|
+
// StigmerProvider the portal container is null — pin it to document.body
|
|
19
|
+
// like sibling tests.
|
|
20
|
+
vi.mock("../../portal-container", () => ({
|
|
21
|
+
useStigmerPortalContainer: () => document.body,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
// happy-dom lacks ResizeObserver, which Base UI positioners observe.
|
|
26
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
27
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
28
|
+
class {
|
|
29
|
+
observe() {}
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(cleanup);
|
|
37
|
+
|
|
38
|
+
/** Frozen reference instant — fixtures must never read the real clock. */
|
|
39
|
+
const NOW = new Date("2026-07-20T11:00:00Z");
|
|
40
|
+
|
|
41
|
+
const hoursAgo = (h: number) => new Date(NOW.getTime() - h * 3_600_000);
|
|
42
|
+
|
|
43
|
+
const ENTRIES: readonly RecentActivityEntry[] = [
|
|
44
|
+
{
|
|
45
|
+
id: "ses_today",
|
|
46
|
+
type: "session",
|
|
47
|
+
subject: "Draft email copy for the Q3 launch",
|
|
48
|
+
updatedAt: hoursAgo(2),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "wex_yesterday",
|
|
52
|
+
type: "workflow_execution",
|
|
53
|
+
subject: "Nightly refund sweep",
|
|
54
|
+
updatedAt: hoursAgo(26),
|
|
55
|
+
status: "failed",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The default consumer seam under test: an anchor per row, tagged with the
|
|
61
|
+
* row id so assertions can address rows the way real consumers do.
|
|
62
|
+
*/
|
|
63
|
+
const renderAnchor: RenderSidebarLink = ({
|
|
64
|
+
id,
|
|
65
|
+
href,
|
|
66
|
+
className,
|
|
67
|
+
children,
|
|
68
|
+
"aria-current": ariaCurrent,
|
|
69
|
+
}) => (
|
|
70
|
+
<a href={href} data-row-id={id} aria-current={ariaCurrent} className={className}>
|
|
71
|
+
{children}
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function renderSidebar(ui: ReactNode) {
|
|
76
|
+
const client = new Stigmer({
|
|
77
|
+
baseUrl: "/",
|
|
78
|
+
getAccessToken: () => "test-token",
|
|
79
|
+
customTransport: createRouterTransport((router) => {
|
|
80
|
+
router.service(OrganizationQueryController, {
|
|
81
|
+
findMyOrganizations: () =>
|
|
82
|
+
create(OrganizationsSchema, {
|
|
83
|
+
entries: [
|
|
84
|
+
create(OrganizationSchema, {
|
|
85
|
+
metadata: create(ApiResourceMetadataSchema, {
|
|
86
|
+
id: "org_acme",
|
|
87
|
+
slug: "acme",
|
|
88
|
+
name: "Acme Corp",
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
return render(
|
|
97
|
+
<StigmerContext.Provider value={client}>
|
|
98
|
+
<OrgProvider>{ui}</OrgProvider>
|
|
99
|
+
</StigmerContext.Provider>,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function baseProps() {
|
|
104
|
+
return {
|
|
105
|
+
renderLink: renderAnchor,
|
|
106
|
+
recentActivity: { entries: ENTRIES },
|
|
107
|
+
footer: <span data-testid="footer-slot">footer</span>,
|
|
108
|
+
now: NOW,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("WorkspaceSidebar — primary navigation", () => {
|
|
113
|
+
it("renders the three primary rows through renderLink with stable ids and hrefs", () => {
|
|
114
|
+
const { container } = renderSidebar(<WorkspaceSidebar {...baseProps()} />);
|
|
115
|
+
|
|
116
|
+
const row = (id: string) =>
|
|
117
|
+
container.querySelector<HTMLAnchorElement>(`[data-row-id="${id}"]`);
|
|
118
|
+
expect(row("new-session")?.getAttribute("href")).toBe("/");
|
|
119
|
+
expect(row("dashboard")?.getAttribute("href")).toBe("/dashboard");
|
|
120
|
+
expect(row("library")?.getAttribute("href")).toBe("/library");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("marks only the active row with aria-current and the accent classes", () => {
|
|
124
|
+
const { container } = renderSidebar(
|
|
125
|
+
<WorkspaceSidebar {...baseProps()} activeNav="library" />,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const library = container.querySelector('[data-row-id="library"]')!;
|
|
129
|
+
expect(library.getAttribute("aria-current")).toBe("page");
|
|
130
|
+
expect(library.className.split(" ")).toContain("bg-sidebar-accent");
|
|
131
|
+
|
|
132
|
+
const dashboard = container.querySelector('[data-row-id="dashboard"]')!;
|
|
133
|
+
expect(dashboard.getAttribute("aria-current")).toBeNull();
|
|
134
|
+
// (hover:bg-sidebar-accent is fine — only the resting accent means active)
|
|
135
|
+
expect(dashboard.className.split(" ")).not.toContain("bg-sidebar-accent");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("WorkspaceSidebar — recents", () => {
|
|
140
|
+
it("buckets entries against the frozen `now`, never the live clock", () => {
|
|
141
|
+
renderSidebar(<WorkspaceSidebar {...baseProps()} />);
|
|
142
|
+
|
|
143
|
+
// 2h ago is Today; 26h ago is Yesterday — only under the frozen NOW.
|
|
144
|
+
expect(screen.getByText("Today")).toBeTruthy();
|
|
145
|
+
expect(screen.getByText("Yesterday")).toBeTruthy();
|
|
146
|
+
expect(screen.getByText("2h")).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("passes the entry to renderLink and derives the viewer route from its type", () => {
|
|
150
|
+
const seen: Record<string, string> = {};
|
|
151
|
+
const spyLink: RenderSidebarLink = (props) => {
|
|
152
|
+
if (props.entry) seen[props.entry.id] = props.href;
|
|
153
|
+
return renderAnchor(props);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
renderSidebar(<WorkspaceSidebar {...baseProps()} renderLink={spyLink} />);
|
|
157
|
+
|
|
158
|
+
expect(seen).toEqual({
|
|
159
|
+
ses_today: "/sessions/ses_today",
|
|
160
|
+
wex_yesterday: "/executions/wex_yesterday",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("highlights the active session row and renders the status badge", () => {
|
|
165
|
+
const { container } = renderSidebar(
|
|
166
|
+
<WorkspaceSidebar {...baseProps()} activeSessionId="ses_today" />,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const active = container.querySelector('[data-row-id="ses_today"]')!;
|
|
170
|
+
expect(active.className).toContain("bg-sidebar-accent");
|
|
171
|
+
expect(active.getAttribute("aria-current")).toBe("page");
|
|
172
|
+
// The failed execution row explains why it is in the list.
|
|
173
|
+
const failed = container.querySelector('[data-row-id="wex_yesterday"]')!;
|
|
174
|
+
expect(within(failed as HTMLElement).getByText("failed")).toBeTruthy();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("renders the per-entry accessory inside its row", () => {
|
|
178
|
+
const { container } = renderSidebar(
|
|
179
|
+
<WorkspaceSidebar
|
|
180
|
+
{...baseProps()}
|
|
181
|
+
renderEntryAccessory={(entry) =>
|
|
182
|
+
entry.id === "ses_today" ? <span data-testid="run-dot" /> : null
|
|
183
|
+
}
|
|
184
|
+
/>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const row = container.querySelector('[data-row-id="ses_today"]')!;
|
|
188
|
+
expect(within(row as HTMLElement).getByTestId("run-dot")).toBeTruthy();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("WorkspaceSidebar — fetch states", () => {
|
|
193
|
+
it("shows skeletons while loading", () => {
|
|
194
|
+
renderSidebar(
|
|
195
|
+
<WorkspaceSidebar
|
|
196
|
+
{...baseProps()}
|
|
197
|
+
recentActivity={{ entries: [], isLoading: true }}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
expect(screen.getByLabelText("Loading sessions")).toBeTruthy();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("shows the error alert plus the empty state on failure", () => {
|
|
204
|
+
renderSidebar(
|
|
205
|
+
<WorkspaceSidebar
|
|
206
|
+
{...baseProps()}
|
|
207
|
+
recentActivity={{ entries: [], error: { message: "backend down" } }}
|
|
208
|
+
/>,
|
|
209
|
+
);
|
|
210
|
+
expect(screen.getByRole("alert").textContent).toBe("backend down");
|
|
211
|
+
expect(screen.getByText("No recent activity")).toBeTruthy();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("shows the empty state when there are no entries", () => {
|
|
215
|
+
renderSidebar(
|
|
216
|
+
<WorkspaceSidebar {...baseProps()} recentActivity={{ entries: [] }} />,
|
|
217
|
+
);
|
|
218
|
+
expect(screen.getByText("No recent activity")).toBeTruthy();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("WorkspaceSidebar — chrome", () => {
|
|
223
|
+
it("renders the footer slot and the collapse toggle with aria state", () => {
|
|
224
|
+
renderSidebar(<WorkspaceSidebar {...baseProps()} isOpen={false} />);
|
|
225
|
+
|
|
226
|
+
expect(screen.getByTestId("footer-slot")).toBeTruthy();
|
|
227
|
+
const toggle = screen.getByLabelText("Collapse sidebar");
|
|
228
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
|
229
|
+
expect(toggle.getAttribute("aria-controls")).toBe("sidebar");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("resolves the org switcher from the mocked transport", async () => {
|
|
233
|
+
renderSidebar(<WorkspaceSidebar {...baseProps()} />);
|
|
234
|
+
expect(await screen.findByText("Acme Corp")).toBeTruthy();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { PanelLeft } from "lucide-react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
7
|
+
import { OrgSwitcher } from "../organization/OrgSwitcher.js";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Shared sidebar chrome — the pieces WorkspaceSidebar and SettingsSidebar
|
|
11
|
+
// have in common: the nav container, the top row (collapse toggle + org
|
|
12
|
+
// switcher), the hairline separator, the footer band, and the one nav-row
|
|
13
|
+
// class recipe. Internal to the sidebar domain; not exported from the SDK.
|
|
14
|
+
//
|
|
15
|
+
// Every class here is transcribed from the console's sidebars — this file
|
|
16
|
+
// IS the console's chrome now, so a change here changes the console, the
|
|
17
|
+
// desktop app, and every documentation tour in one place.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** Props shared by both sidebar variants' outer chrome. */
|
|
21
|
+
export interface SidebarChromeProps {
|
|
22
|
+
/** Accessible name for the `<nav>` landmark. */
|
|
23
|
+
readonly ariaLabel: string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the sidebar is currently expanded, reflected as
|
|
26
|
+
* `aria-expanded` on the collapse toggle. @default true
|
|
27
|
+
*/
|
|
28
|
+
readonly isOpen?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the collapse toggle is pressed. The sidebar renders the
|
|
31
|
+
* toggle either way (it is part of the depicted chrome); hosts that
|
|
32
|
+
* cannot collapse (fixed embeds) simply omit the handler.
|
|
33
|
+
*/
|
|
34
|
+
readonly onCollapse?: () => void;
|
|
35
|
+
/** Passed through to {@link OrgSwitcher}. */
|
|
36
|
+
readonly onOrgChanged?: (org: Organization) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Footer content — typically the host app's `UserMenu` wrapper, which
|
|
39
|
+
* bridges auth, theming, and routing. A slot rather than a built-in
|
|
40
|
+
* because every host wires those systems differently (DD-004).
|
|
41
|
+
*/
|
|
42
|
+
readonly footer: ReactNode;
|
|
43
|
+
readonly children: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Outer sidebar frame: `<nav>` container, top row, content, footer band.
|
|
48
|
+
*
|
|
49
|
+
* The `id="sidebar"` / `aria-controls` pairing matches the console's DOM
|
|
50
|
+
* contract (the app shell's floating "Open sidebar" button targets it);
|
|
51
|
+
* only one sidebar renders at a time, so the id cannot collide.
|
|
52
|
+
*/
|
|
53
|
+
export function SidebarChrome({
|
|
54
|
+
ariaLabel,
|
|
55
|
+
isOpen = true,
|
|
56
|
+
onCollapse,
|
|
57
|
+
onOrgChanged,
|
|
58
|
+
footer,
|
|
59
|
+
children,
|
|
60
|
+
}: SidebarChromeProps) {
|
|
61
|
+
return (
|
|
62
|
+
<nav
|
|
63
|
+
id="sidebar"
|
|
64
|
+
aria-label={ariaLabel}
|
|
65
|
+
className="bg-sidebar text-sidebar-foreground flex h-full flex-col"
|
|
66
|
+
>
|
|
67
|
+
{/* Top row: collapse toggle + org context */}
|
|
68
|
+
<div className="flex flex-none items-center gap-1 px-2 py-2">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onCollapse}
|
|
72
|
+
aria-expanded={isOpen}
|
|
73
|
+
aria-controls="sidebar"
|
|
74
|
+
aria-label="Collapse sidebar"
|
|
75
|
+
className={cn(
|
|
76
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-md",
|
|
77
|
+
"text-sidebar-foreground transition-colors motion-reduce:transition-none",
|
|
78
|
+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
79
|
+
"aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground",
|
|
80
|
+
"focus-visible:ring-sidebar-ring focus-visible:ring-2 focus-visible:outline-none",
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<PanelLeft className="size-4" />
|
|
84
|
+
</button>
|
|
85
|
+
<div className="min-w-0 flex-1">
|
|
86
|
+
<OrgSwitcher onOrgChanged={onOrgChanged} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{children}
|
|
91
|
+
|
|
92
|
+
{/* Bottom: user menu */}
|
|
93
|
+
<div className="border-sidebar-border flex-none border-t px-3 py-2">
|
|
94
|
+
{footer}
|
|
95
|
+
</div>
|
|
96
|
+
</nav>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Hairline separator between sidebar sections. */
|
|
101
|
+
export function SidebarSeparator() {
|
|
102
|
+
return (
|
|
103
|
+
<div className="px-3 py-1">
|
|
104
|
+
<div className="bg-sidebar-border h-px" />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The one nav-row class recipe both sidebars share: 14px medium labels,
|
|
111
|
+
* 16px icons (sized by callers), 8px/6px padding, rounded row highlight.
|
|
112
|
+
*
|
|
113
|
+
* `muted` renders the resting label in the muted sidebar tone — used by
|
|
114
|
+
* "Back to Sessions", which is an exit, not a destination.
|
|
115
|
+
*/
|
|
116
|
+
export function navRowClassName(
|
|
117
|
+
active: boolean,
|
|
118
|
+
{ muted = false }: { muted?: boolean } = {},
|
|
119
|
+
): string {
|
|
120
|
+
return cn(
|
|
121
|
+
"flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium transition-colors",
|
|
122
|
+
active
|
|
123
|
+
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
124
|
+
: cn(
|
|
125
|
+
muted ? "text-sidebar-muted-foreground" : "text-sidebar-foreground",
|
|
126
|
+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Sidebar — the console's navigation chrome as shared SDK components.
|
|
2
|
+
//
|
|
3
|
+
// One source of truth for the sidebar the web console, desktop app, and
|
|
4
|
+
// documentation tours all render (stigmer/stigmer#317): hosts inject their
|
|
5
|
+
// router via `renderLink`, their data via props, and their user menu via
|
|
6
|
+
// the `footer` slot.
|
|
7
|
+
|
|
8
|
+
export { WorkspaceSidebar } from "./WorkspaceSidebar.js";
|
|
9
|
+
export type {
|
|
10
|
+
WorkspaceSidebarProps,
|
|
11
|
+
WorkspaceSidebarActivity,
|
|
12
|
+
WorkspaceNavId,
|
|
13
|
+
} from "./WorkspaceSidebar.js";
|
|
14
|
+
export { SettingsSidebar } from "./SettingsSidebar.js";
|
|
15
|
+
export type { SettingsSidebarProps } from "./SettingsSidebar.js";
|
|
16
|
+
export type { SidebarLinkRenderProps, RenderSidebarLink } from "./types.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { RecentActivityEntry } from "../activity/types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Everything a sidebar hands to {@link RenderSidebarLink} to draw one
|
|
6
|
+
* interactive row.
|
|
7
|
+
*
|
|
8
|
+
* The sidebar owns the row's look (`className`, `children`) and state
|
|
9
|
+
* (`active`, `aria-current`); the consumer owns only the interactive
|
|
10
|
+
* element wrapping them. That split is what keeps one component serving
|
|
11
|
+
* three very different hosts: the web console returns `next/link`, the
|
|
12
|
+
* desktop app returns a `react-router` link, and documentation embeds
|
|
13
|
+
* return inert rows decorated with capture markers.
|
|
14
|
+
*/
|
|
15
|
+
export interface SidebarLinkRenderProps {
|
|
16
|
+
/**
|
|
17
|
+
* Stable row identifier, for consumers that vary behavior per row:
|
|
18
|
+
* `"new-session"` / `"dashboard"` / `"library"` in the workspace
|
|
19
|
+
* sidebar, `"back-to-sessions"` or the settings route basename (e.g.
|
|
20
|
+
* `"api-keys"`) in the settings sidebar, and the entry id for recent
|
|
21
|
+
* activity rows.
|
|
22
|
+
*/
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Console route this row targets (e.g. `/library`, `/sessions/<id>`). */
|
|
25
|
+
readonly href: string;
|
|
26
|
+
/** Whether the row is the current location. */
|
|
27
|
+
readonly active: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Fully-styled row classes computed by the sidebar. Apply verbatim —
|
|
30
|
+
* the row's geometry and type scale are the product surface tours and
|
|
31
|
+
* embeds depict, so consumers must not restyle them.
|
|
32
|
+
*/
|
|
33
|
+
readonly className: string;
|
|
34
|
+
/** `"page"` when active, for assistive tech. Spread onto the element. */
|
|
35
|
+
readonly "aria-current": "page" | undefined;
|
|
36
|
+
/** Row content (icon, label, timestamps). Render inside the element. */
|
|
37
|
+
readonly children: ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* The recent-activity entry behind this row. Present only for recents
|
|
40
|
+
* rows in the workspace sidebar — consumers use it to pick the right
|
|
41
|
+
* navigation flow (session viewer vs. execution viewer) without
|
|
42
|
+
* parsing `href`.
|
|
43
|
+
*/
|
|
44
|
+
readonly entry?: RecentActivityEntry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renders one sidebar row as the consumer's interactive element.
|
|
49
|
+
*
|
|
50
|
+
* Must return a single element that spreads unknown props onto its
|
|
51
|
+
* underlying DOM node (all DOM elements, `next/link`, and `react-router`
|
|
52
|
+
* links qualify): the sidebar composes tooltip triggers around recents
|
|
53
|
+
* rows by cloning the returned element with merged event handlers.
|
|
54
|
+
*/
|
|
55
|
+
export type RenderSidebarLink = (props: SidebarLinkRenderProps) => ReactElement;
|
|
@@ -17,6 +17,8 @@ export interface UseSkillCountOptions {
|
|
|
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 useSkillCount}. */
|