@stigmer/react 3.5.3 → 3.7.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/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- package/composer/ComposerToolbar.d.ts +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- package/composer/SessionComposer.js.map +1 -1
- package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +10 -1
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- package/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/models/ModelSelector.d.ts +26 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +27 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +10 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -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/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +81 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +648 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +46 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +214 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +37 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +19 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +41 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +62 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/agent/AgentPicker.tsx +13 -1
- package/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +70 -0
- package/src/internal/Pagination.tsx +72 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/models/ModelSelector.tsx +100 -3
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +16 -0
- package/src/models/service-tier.ts +49 -0
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +1651 -0
- package/src/schedule/ScheduleForm.tsx +615 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +69 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +90 -0
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { ModelSelector } from "../ModelSelector";
|
|
4
|
+
import { ModelRegistryContext } from "../ModelRegistryContext";
|
|
5
|
+
import type { ModelInfo } from "../registry";
|
|
6
|
+
|
|
7
|
+
// Portal into document.body so the popover mounts under happy-dom, and shim
|
|
8
|
+
// ResizeObserver for Base UI's positioner (the scheduleCreation.test.tsx
|
|
9
|
+
// pattern).
|
|
10
|
+
vi.mock("../../portal-container", () => ({
|
|
11
|
+
useStigmerPortalContainer: () => document.body,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
16
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
17
|
+
class {
|
|
18
|
+
observe() {}
|
|
19
|
+
unobserve() {}
|
|
20
|
+
disconnect() {}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Service-tier toggle gating (stigmer/stigmer#357).
|
|
27
|
+
*
|
|
28
|
+
* The toggle is capability-gated: it renders only when the consumer opted in
|
|
29
|
+
* (onServiceTierChange) AND the selected model prices a fast variant
|
|
30
|
+
* (ModelInfo.serviceTiers). Switching to a model without a fast tier resets
|
|
31
|
+
* an active fast selection — a stale tier would be refused at create time.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const FAST_CAPABLE: ModelInfo = {
|
|
35
|
+
modelId: "composer-2.5",
|
|
36
|
+
provider: "cursor",
|
|
37
|
+
displayName: "Composer 2.5",
|
|
38
|
+
shortDescription: "Fast agentic model",
|
|
39
|
+
speedTier: "fastest",
|
|
40
|
+
costTier: "economy",
|
|
41
|
+
harness: "cursor",
|
|
42
|
+
featured: true,
|
|
43
|
+
serviceTiers: ["fast"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const NO_FAST_TIER: ModelInfo = {
|
|
47
|
+
modelId: "claude-haiku-4-5",
|
|
48
|
+
provider: "anthropic",
|
|
49
|
+
displayName: "Haiku 4.5",
|
|
50
|
+
shortDescription: "Small fast model",
|
|
51
|
+
speedTier: "fastest",
|
|
52
|
+
costTier: "economy",
|
|
53
|
+
harness: "cursor",
|
|
54
|
+
featured: true,
|
|
55
|
+
serviceTiers: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function renderSelector(
|
|
59
|
+
props?: Partial<React.ComponentProps<typeof ModelSelector>>,
|
|
60
|
+
) {
|
|
61
|
+
const onValueChange = vi.fn();
|
|
62
|
+
const onServiceTierChange = vi.fn();
|
|
63
|
+
|
|
64
|
+
const result = render(
|
|
65
|
+
<ModelRegistryContext.Provider
|
|
66
|
+
value={{
|
|
67
|
+
models: [FAST_CAPABLE, NO_FAST_TIER],
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null,
|
|
70
|
+
refetch: vi.fn(),
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<ModelSelector
|
|
74
|
+
value="composer-2.5"
|
|
75
|
+
onValueChange={onValueChange}
|
|
76
|
+
harness="cursor"
|
|
77
|
+
serviceTier="standard"
|
|
78
|
+
onServiceTierChange={onServiceTierChange}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</ModelRegistryContext.Provider>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { ...result, onValueChange, onServiceTierChange };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Open the popover via its trigger and wait for the portaled popup to
|
|
89
|
+
* mount (Base UI portals render asynchronously in jsdom). The search
|
|
90
|
+
* input is the popup's always-present landmark.
|
|
91
|
+
*/
|
|
92
|
+
async function openPopover(triggerLabel: string) {
|
|
93
|
+
fireEvent.click(screen.getByRole("button", { name: new RegExp(triggerLabel) }));
|
|
94
|
+
await screen.findByPlaceholderText("Search models…");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A model row inside the popup list (never matches the trigger). */
|
|
98
|
+
function modelOption(displayName: string): HTMLElement {
|
|
99
|
+
const option = screen
|
|
100
|
+
.getAllByText(displayName)
|
|
101
|
+
.map((el) => el.closest<HTMLElement>("[data-model-option]"))
|
|
102
|
+
.find((el) => el != null);
|
|
103
|
+
if (!option) throw new Error(`No popup option for "${displayName}"`);
|
|
104
|
+
return option;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
afterEach(cleanup);
|
|
108
|
+
|
|
109
|
+
describe("ModelSelector — service tier toggle", () => {
|
|
110
|
+
it("renders the toggle for a fast-capable model", async () => {
|
|
111
|
+
renderSelector();
|
|
112
|
+
await openPopover("Composer 2.5");
|
|
113
|
+
|
|
114
|
+
const toggle = screen.getByRole("switch", { name: "Fast tier" });
|
|
115
|
+
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not render the toggle for a model without a fast tier", async () => {
|
|
119
|
+
renderSelector({ value: "claude-haiku-4-5" });
|
|
120
|
+
await openPopover("Haiku 4.5");
|
|
121
|
+
|
|
122
|
+
expect(screen.queryByRole("switch", { name: "Fast tier" })).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not render the toggle when the consumer did not opt in", async () => {
|
|
126
|
+
renderSelector({ onServiceTierChange: undefined });
|
|
127
|
+
await openPopover("Composer 2.5");
|
|
128
|
+
|
|
129
|
+
expect(screen.queryByRole("switch", { name: "Fast tier" })).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("toggling fires onServiceTierChange with the flipped tier", async () => {
|
|
133
|
+
const { onServiceTierChange } = renderSelector();
|
|
134
|
+
await openPopover("Composer 2.5");
|
|
135
|
+
|
|
136
|
+
fireEvent.click(screen.getByRole("switch", { name: "Fast tier" }));
|
|
137
|
+
expect(onServiceTierChange).toHaveBeenCalledOnce();
|
|
138
|
+
expect(onServiceTierChange).toHaveBeenCalledWith("fast");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shows the Fast badge on the trigger when the fast tier is active", () => {
|
|
142
|
+
renderSelector({ serviceTier: "fast" });
|
|
143
|
+
|
|
144
|
+
expect(screen.getByText("Fast")).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("resets an active fast tier when selecting a model without one", async () => {
|
|
148
|
+
const { onValueChange, onServiceTierChange } = renderSelector({
|
|
149
|
+
serviceTier: "fast",
|
|
150
|
+
});
|
|
151
|
+
await openPopover("Composer 2.5");
|
|
152
|
+
|
|
153
|
+
fireEvent.click(modelOption("Haiku 4.5"));
|
|
154
|
+
|
|
155
|
+
expect(onValueChange).toHaveBeenCalledOnce();
|
|
156
|
+
expect(onValueChange).toHaveBeenCalledWith("claude-haiku-4-5");
|
|
157
|
+
expect(onServiceTierChange).toHaveBeenCalledOnce();
|
|
158
|
+
expect(onServiceTierChange).toHaveBeenCalledWith("standard");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps an active fast tier when selecting another fast-capable model", async () => {
|
|
162
|
+
const { onServiceTierChange } = renderSelector({
|
|
163
|
+
serviceTier: "fast",
|
|
164
|
+
value: "claude-haiku-4-5",
|
|
165
|
+
});
|
|
166
|
+
await openPopover("Haiku 4.5");
|
|
167
|
+
|
|
168
|
+
fireEvent.click(modelOption("Composer 2.5"));
|
|
169
|
+
|
|
170
|
+
expect(onServiceTierChange).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -73,6 +73,9 @@ const TEST_REGISTRY_JSON = {
|
|
|
73
73
|
costTier: "standard",
|
|
74
74
|
featured: false,
|
|
75
75
|
pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 },
|
|
76
|
+
pricingVariants: {
|
|
77
|
+
fast: { inputPricePerMillion: 18, outputPricePerMillion: 90 },
|
|
78
|
+
},
|
|
76
79
|
},
|
|
77
80
|
{
|
|
78
81
|
id: "ollama-local",
|
|
@@ -103,6 +106,18 @@ function createWrapper(models: readonly ModelInfo[] = TEST_MODELS) {
|
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
describe("parseRegistryJson service tiers (#357)", () => {
|
|
110
|
+
it("surfaces pricingVariants keys as serviceTiers", () => {
|
|
111
|
+
const withVariants = TEST_MODELS.find((m) => m.modelId === "claude-4.6-sonnet");
|
|
112
|
+
expect(withVariants?.serviceTiers).toEqual(["fast"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("yields an empty serviceTiers list for models without variants", () => {
|
|
116
|
+
const withoutVariants = TEST_MODELS.find((m) => m.modelId === "claude-sonnet-4.6");
|
|
117
|
+
expect(withoutVariants?.serviceTiers).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
106
121
|
describe("useModelRegistry", () => {
|
|
107
122
|
describe("unified mode (no harness)", () => {
|
|
108
123
|
it("excludes DISABLED_PROVIDERS from models", () => {
|
package/src/models/index.ts
CHANGED
|
@@ -11,3 +11,5 @@ export { HarnessSelector } from "./HarnessSelector.js";
|
|
|
11
11
|
export type { HarnessSelectorProps } from "./HarnessSelector.js";
|
|
12
12
|
export { DEFAULT_HARNESS, HARNESS_LABELS, HARNESS_META, HARNESS_OPTIONS, toProtoHarness, fromProtoHarness } from "./harness.js";
|
|
13
13
|
export type { HarnessOption, HarnessDisplayInfo } from "./harness.js";
|
|
14
|
+
export { FAST_SERVICE_TIER, toProtoServiceTier, fromProtoServiceTier } from "./service-tier.js";
|
|
15
|
+
export type { ServiceTierOption } from "./service-tier.js";
|
package/src/models/registry.ts
CHANGED
|
@@ -89,6 +89,16 @@ export interface ModelInfo {
|
|
|
89
89
|
* before "Show All Models" is expanded or search is used).
|
|
90
90
|
*/
|
|
91
91
|
readonly featured: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Pricing-variant keys the registry prices for this model (e.g.
|
|
94
|
+
* `["fast"]`). A priced variant is a *selectable* service tier
|
|
95
|
+
* (stigmer/stigmer#357): the tier toggle renders only for models whose
|
|
96
|
+
* `serviceTiers` includes it. Empty for models with no variants.
|
|
97
|
+
*
|
|
98
|
+
* Distinct from {@link speedTier}, which is a static latency badge,
|
|
99
|
+
* not a selectable option.
|
|
100
|
+
*/
|
|
101
|
+
readonly serviceTiers: readonly string[];
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -153,6 +163,8 @@ interface RegistryJsonEntry {
|
|
|
153
163
|
cacheWritePricePerMillion: number;
|
|
154
164
|
cacheReadPricePerMillion: number;
|
|
155
165
|
};
|
|
166
|
+
/** Variant-key → variant pricing block; only the key set matters here. */
|
|
167
|
+
pricingVariants?: Record<string, unknown>;
|
|
156
168
|
$comment?: string;
|
|
157
169
|
}
|
|
158
170
|
|
|
@@ -192,6 +204,10 @@ export function parseRegistryJson(data: unknown): ModelInfo[] {
|
|
|
192
204
|
costTier: m.costTier as CostTier,
|
|
193
205
|
harness: m.harness as HarnessOption,
|
|
194
206
|
featured: m.featured ?? false,
|
|
207
|
+
serviceTiers:
|
|
208
|
+
m.pricingVariants && typeof m.pricingVariants === "object"
|
|
209
|
+
? Object.keys(m.pricingVariants).sort()
|
|
210
|
+
: [],
|
|
195
211
|
}));
|
|
196
212
|
}
|
|
197
213
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* String form of the service tier for component props and hook inputs
|
|
5
|
+
* (stigmer/stigmer#357). Mirrors {@link HarnessOption}: components speak
|
|
6
|
+
* strings, execution creation speaks proto.
|
|
7
|
+
*
|
|
8
|
+
* "standard" is the model's base-priced configuration; "fast" is the
|
|
9
|
+
* provider's fast variant at the registry's fast rates — selectable only
|
|
10
|
+
* for models whose {@link ModelInfo.serviceTiers} includes "fast".
|
|
11
|
+
*/
|
|
12
|
+
export type ServiceTierOption = "standard" | "fast";
|
|
13
|
+
|
|
14
|
+
/** The tier key a model must price for the fast toggle to render. */
|
|
15
|
+
export const FAST_SERVICE_TIER = "fast";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert a {@link ServiceTierOption} string to the proto enum.
|
|
19
|
+
*/
|
|
20
|
+
export function toProtoServiceTier(tier: ServiceTierOption): ServiceTier {
|
|
21
|
+
switch (tier) {
|
|
22
|
+
case "fast":
|
|
23
|
+
return ServiceTier.FAST;
|
|
24
|
+
case "standard":
|
|
25
|
+
default:
|
|
26
|
+
return ServiceTier.STANDARD;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert a proto {@link ServiceTier} enum to its string form.
|
|
32
|
+
*
|
|
33
|
+
* `UNSPECIFIED` (and any unknown value) maps to `undefined` so callers can
|
|
34
|
+
* decide their own default — matching the proto contract (`UNSPECIFIED`
|
|
35
|
+
* resolves to `SERVICE_TIER_STANDARD` in the runner, never the provider
|
|
36
|
+
* account default).
|
|
37
|
+
*/
|
|
38
|
+
export function fromProtoServiceTier(
|
|
39
|
+
tier: ServiceTier | undefined,
|
|
40
|
+
): ServiceTierOption | undefined {
|
|
41
|
+
switch (tier) {
|
|
42
|
+
case ServiceTier.STANDARD:
|
|
43
|
+
return "standard";
|
|
44
|
+
case ServiceTier.FAST:
|
|
45
|
+
return "fast";
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
14
14
|
import { useStigmer } from "../hooks.js";
|
|
15
|
+
import { useFetchCache } from "../internal/FetchCacheProvider.js";
|
|
15
16
|
|
|
16
17
|
/** Value exposed by {@link OrgProvider} via {@link useOrg}. */
|
|
17
18
|
export interface OrgContextValue {
|
|
@@ -63,19 +64,27 @@ function persistSlug(slug: string): void {
|
|
|
63
64
|
* organization selection, and persists the choice to `localStorage`
|
|
64
65
|
* under the key `stigmer:activeOrgSlug`.
|
|
65
66
|
*
|
|
66
|
-
* Must be rendered inside a {@link StigmerProvider}.
|
|
67
|
+
* Must be rendered inside a {@link StigmerProvider}. Mount it BELOW
|
|
68
|
+
* `FetchCacheProvider` (when one is used): switching the active org clears
|
|
69
|
+
* the nearest fetch cache, so view state cached under the previous org
|
|
70
|
+
* context — session and execution entries are keyed by id, not org — can
|
|
71
|
+
* never bleed into the new one. Navigation on switch stays the host's
|
|
72
|
+
* responsibility via `OrgSwitcher`'s `onOrgChanged` callback.
|
|
67
73
|
*
|
|
68
74
|
* @example
|
|
69
75
|
* ```tsx
|
|
70
76
|
* <StigmerProvider client={client}>
|
|
71
|
-
* <
|
|
72
|
-
* <
|
|
73
|
-
*
|
|
77
|
+
* <FetchCacheProvider>
|
|
78
|
+
* <OrgProvider>
|
|
79
|
+
* <App />
|
|
80
|
+
* </OrgProvider>
|
|
81
|
+
* </FetchCacheProvider>
|
|
74
82
|
* </StigmerProvider>
|
|
75
83
|
* ```
|
|
76
84
|
*/
|
|
77
85
|
export function OrgProvider({ children }: { children: ReactNode }) {
|
|
78
86
|
const stigmer = useStigmer();
|
|
87
|
+
const fetchCache = useFetchCache();
|
|
79
88
|
const [orgs, setOrgs] = useState<Organization[]>([]);
|
|
80
89
|
const [activeOrg, setActiveOrgState] = useState<Organization | null>(null);
|
|
81
90
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -140,6 +149,22 @@ export function OrgProvider({ children }: { children: ReactNode }) {
|
|
|
140
149
|
}
|
|
141
150
|
}, []);
|
|
142
151
|
|
|
152
|
+
// Org-switch invariant: view state cached under one org context must not
|
|
153
|
+
// survive into another. Covers every path that changes the active org
|
|
154
|
+
// (explicit switch, post-create refresh) while skipping the initial
|
|
155
|
+
// restore (previous slug is null). Cache keys for sessions/executions are
|
|
156
|
+
// id-scoped, not org-scoped, so a TTL'd entry would otherwise outlive the
|
|
157
|
+
// switch. No-op when no FetchCacheProvider is mounted.
|
|
158
|
+
const previousSlugRef = useRef<string | null>(null);
|
|
159
|
+
const activeSlug = activeOrg?.metadata?.slug ?? null;
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const previous = previousSlugRef.current;
|
|
162
|
+
previousSlugRef.current = activeSlug;
|
|
163
|
+
if (previous !== null && activeSlug !== null && previous !== activeSlug) {
|
|
164
|
+
fetchCache?.clear();
|
|
165
|
+
}
|
|
166
|
+
}, [activeSlug, fetchCache]);
|
|
167
|
+
|
|
143
168
|
const value = useMemo<OrgContextValue>(
|
|
144
169
|
() => ({
|
|
145
170
|
orgs,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { FetchCache } from "../../internal/fetch-cache";
|
|
7
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
8
|
+
import { OrgProvider, useOrg } from "../OrgProvider";
|
|
9
|
+
|
|
10
|
+
const acme = { metadata: { id: "acme", slug: "acme", name: "Acme" } } as Organization;
|
|
11
|
+
const globex = { metadata: { id: "globex", slug: "globex", name: "Globex" } } as Organization;
|
|
12
|
+
|
|
13
|
+
function createMockStigmer(orgs: Organization[]) {
|
|
14
|
+
return {
|
|
15
|
+
organization: {
|
|
16
|
+
findMyOrganizations: vi.fn().mockResolvedValue({ entries: orgs }),
|
|
17
|
+
},
|
|
18
|
+
} as never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapper(client: unknown, cache: FetchCache | null) {
|
|
22
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<StigmerContext.Provider value={client as never}>
|
|
25
|
+
<FetchCacheContext.Provider value={cache}>
|
|
26
|
+
<OrgProvider>{children}</OrgProvider>
|
|
27
|
+
</FetchCacheContext.Provider>
|
|
28
|
+
</StigmerContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("OrgProvider — org-switch cache invalidation", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
localStorage.clear();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("clears the fetch cache when the active org changes", async () => {
|
|
39
|
+
const cache = new FetchCache();
|
|
40
|
+
cache.set("session:ses_1", { stale: "from previous org context" });
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useOrg(), {
|
|
43
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), cache),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
47
|
+
|
|
48
|
+
// Initial restore must NOT clear — only a genuine change of context does.
|
|
49
|
+
// Session/execution cache keys are id-scoped (not org-scoped), so without
|
|
50
|
+
// the clear a switched-away org's view state would survive its 5-min TTL.
|
|
51
|
+
expect(cache.has("session:ses_1")).toBe(true);
|
|
52
|
+
|
|
53
|
+
act(() => result.current.setActiveOrg(globex));
|
|
54
|
+
|
|
55
|
+
await waitFor(() => expect(cache.has("session:ses_1")).toBe(false));
|
|
56
|
+
expect(result.current.activeOrg?.metadata?.slug).toBe("globex");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not clear when re-selecting the already-active org", async () => {
|
|
60
|
+
const cache = new FetchCache();
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useOrg(), {
|
|
63
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), cache),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
67
|
+
const active = result.current.activeOrg!;
|
|
68
|
+
|
|
69
|
+
cache.set("session:ses_1", { fresh: true });
|
|
70
|
+
act(() => result.current.setActiveOrg(active));
|
|
71
|
+
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
73
|
+
expect(cache.has("session:ses_1")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("works without a FetchCacheProvider (no-op, no crash)", async () => {
|
|
77
|
+
const { result } = renderHook(() => useOrg(), {
|
|
78
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), null),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
82
|
+
|
|
83
|
+
act(() => result.current.setActiveOrg(globex));
|
|
84
|
+
|
|
85
|
+
await waitFor(() =>
|
|
86
|
+
expect(result.current.activeOrg?.metadata?.slug).toBe("globex"),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -6,7 +6,12 @@ import { toError } from "../internal/toError.js";
|
|
|
6
6
|
import { toast } from "../feedback/toast.js";
|
|
7
7
|
|
|
8
8
|
/** Resource types that support deletion from detail pages. */
|
|
9
|
-
export type DeletableResourceKind =
|
|
9
|
+
export type DeletableResourceKind =
|
|
10
|
+
| "agent"
|
|
11
|
+
| "skill"
|
|
12
|
+
| "mcpServer"
|
|
13
|
+
| "workflow"
|
|
14
|
+
| "schedule";
|
|
10
15
|
|
|
11
16
|
export interface UseDeleteResourceReturn {
|
|
12
17
|
/**
|
|
@@ -74,6 +79,9 @@ export function useDeleteResource(
|
|
|
74
79
|
case "workflow":
|
|
75
80
|
await stigmer.workflow.delete(resourceId);
|
|
76
81
|
break;
|
|
82
|
+
case "schedule":
|
|
83
|
+
await stigmer.schedule.delete(resourceId);
|
|
84
|
+
break;
|
|
77
85
|
}
|
|
78
86
|
toast.success(
|
|
79
87
|
resourceName
|
|
@@ -106,5 +114,7 @@ function kindLabel(kind: DeletableResourceKind): string {
|
|
|
106
114
|
return "MCP server";
|
|
107
115
|
case "workflow":
|
|
108
116
|
return "workflow";
|
|
117
|
+
case "schedule":
|
|
118
|
+
return "schedule";
|
|
109
119
|
}
|
|
110
120
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, renderHook, screen, waitFor, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { ListResult } from "@stigmer/sdk";
|
|
5
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
6
|
+
import { ResourceWorkbench } from "../components/ResourceWorkbench";
|
|
7
|
+
import { useResourceCollection } from "../hooks/useResourceCollection";
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
function Wrapper({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<FetchCacheContext.Provider value={null}>
|
|
14
|
+
{children}
|
|
15
|
+
</FetchCacheContext.Provider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A row shaped like a resource proto: the id lives under metadata. */
|
|
20
|
+
interface ProtoLikeRow {
|
|
21
|
+
readonly metadata: { readonly id: string; readonly slug: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listFnReturning(entries: unknown[], totalCount = entries.length) {
|
|
25
|
+
return vi.fn(
|
|
26
|
+
async (): Promise<ListResult> => ({
|
|
27
|
+
entries: entries as ListResult["entries"],
|
|
28
|
+
totalCount,
|
|
29
|
+
totalPages: 1,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("ResourceWorkbench searchable prop", () => {
|
|
35
|
+
it("renders the search input by default", async () => {
|
|
36
|
+
render(
|
|
37
|
+
<ResourceWorkbench listFn={listFnReturning([])} org="acme" />,
|
|
38
|
+
{ wrapper: Wrapper },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await waitFor(() =>
|
|
42
|
+
expect(screen.getByRole("textbox", { name: "Search…" })).toBeTruthy(),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("hides the search input when searchable is false", async () => {
|
|
47
|
+
render(
|
|
48
|
+
<ResourceWorkbench
|
|
49
|
+
listFn={listFnReturning([])}
|
|
50
|
+
org="acme"
|
|
51
|
+
searchable={false}
|
|
52
|
+
/>,
|
|
53
|
+
{ wrapper: Wrapper },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await waitFor(() => expect(screen.getByText("No resources found")).toBeTruthy());
|
|
57
|
+
expect(screen.queryByRole("textbox")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("useResourceCollection row identity", () => {
|
|
62
|
+
const protoRows: ProtoLikeRow[] = [
|
|
63
|
+
{ metadata: { id: "sch_a", slug: "a" } },
|
|
64
|
+
{ metadata: { id: "sch_b", slug: "b" } },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const columns = [
|
|
68
|
+
{
|
|
69
|
+
id: "slug",
|
|
70
|
+
header: "Slug",
|
|
71
|
+
cell: (row: ProtoLikeRow) => row.metadata.slug,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
it("uses getItemId for rows without a top-level id", async () => {
|
|
76
|
+
// Hoisted: a listFn re-created per render would retrigger useFetch.
|
|
77
|
+
const listFn = listFnReturning(protoRows);
|
|
78
|
+
const { result } = renderHook(
|
|
79
|
+
() =>
|
|
80
|
+
useResourceCollection<ProtoLikeRow>({
|
|
81
|
+
listFn,
|
|
82
|
+
org: "acme",
|
|
83
|
+
columns,
|
|
84
|
+
getItemId: (row) => row.metadata.id,
|
|
85
|
+
}),
|
|
86
|
+
{ wrapper: Wrapper },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(result.current.items).toHaveLength(2));
|
|
90
|
+
const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
|
|
91
|
+
expect(rowIds).toEqual(["sch_a", "sch_b"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("falls back to SearchResult.id, then the row index", async () => {
|
|
95
|
+
const mixed = [{ id: "res_1" }, { name: "no-id" }];
|
|
96
|
+
const listFn = listFnReturning(mixed);
|
|
97
|
+
const fallbackColumns = [{ id: "c", header: "C", cell: () => null }];
|
|
98
|
+
const { result } = renderHook(
|
|
99
|
+
() =>
|
|
100
|
+
useResourceCollection({
|
|
101
|
+
listFn,
|
|
102
|
+
org: "acme",
|
|
103
|
+
columns: fallbackColumns,
|
|
104
|
+
}),
|
|
105
|
+
{ wrapper: Wrapper },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await waitFor(() => expect(result.current.items).toHaveLength(2));
|
|
109
|
+
const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
|
|
110
|
+
expect(rowIds).toEqual(["res_1", "1"]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -155,6 +155,15 @@ export interface ResourceWorkbenchProps<TData = SearchResult> {
|
|
|
155
155
|
|
|
156
156
|
// --- Layout ------------------------------------------------------------
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether the toolbar renders the text-search input. Set `false` for
|
|
160
|
+
* kinds whose list backend has no text search (e.g. schedules, which
|
|
161
|
+
* use the direct query instead of the search service) — a search box
|
|
162
|
+
* that silently matches nothing is worse than none (DD-006). Opt-in
|
|
163
|
+
* per DD-011: existing consumers keep the search box.
|
|
164
|
+
* @default true
|
|
165
|
+
*/
|
|
166
|
+
readonly searchable?: boolean;
|
|
158
167
|
/** Search input placeholder text. @default "Search\u2026" */
|
|
159
168
|
readonly searchPlaceholder?: string;
|
|
160
169
|
/** Accessible label for the workbench region. @default "Resource workbench" */
|
|
@@ -228,6 +237,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
228
237
|
emptyAction,
|
|
229
238
|
onRetry,
|
|
230
239
|
headerAction,
|
|
240
|
+
searchable = true,
|
|
231
241
|
searchPlaceholder = "Search\u2026",
|
|
232
242
|
"aria-label": ariaLabel = "Resource workbench",
|
|
233
243
|
className,
|
|
@@ -267,6 +277,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
267
277
|
onSortChange: filtersHook.setSort,
|
|
268
278
|
columns: columns as WorkbenchColumnDef<TData>[],
|
|
269
279
|
enableSelection,
|
|
280
|
+
getItemId,
|
|
270
281
|
refetchToken,
|
|
271
282
|
});
|
|
272
283
|
|
|
@@ -305,21 +316,27 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
305
316
|
>
|
|
306
317
|
{/* --- Toolbar: search + filters + view switcher --- */}
|
|
307
318
|
<div className="flex flex-wrap items-center gap-2">
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
319
|
+
{searchable ? (
|
|
320
|
+
<div className="relative flex-1">
|
|
321
|
+
<SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground-subtle" />
|
|
322
|
+
<input
|
|
323
|
+
type="text"
|
|
324
|
+
value={filtersHook.query}
|
|
325
|
+
onChange={(e) => filtersHook.setQuery(e.target.value)}
|
|
326
|
+
placeholder={searchPlaceholder}
|
|
327
|
+
aria-label={searchPlaceholder}
|
|
328
|
+
className={cn(
|
|
329
|
+
"w-full rounded-md border border-input bg-background py-1.5 pl-8 pr-3 text-sm",
|
|
330
|
+
"placeholder:text-muted-foreground-subtle",
|
|
331
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
332
|
+
)}
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
) : (
|
|
336
|
+
// Spacer keeps the scope toggle / view switcher / header action
|
|
337
|
+
// right-aligned, matching the searchable layout.
|
|
338
|
+
<div className="flex-1" aria-hidden="true" />
|
|
339
|
+
)}
|
|
323
340
|
{onScopeChange && (
|
|
324
341
|
<ScopeToggle value={controlledScope} onChange={onScopeChange} />
|
|
325
342
|
)}
|