@codemation/next-host 0.0.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.
Files changed (206) hide show
  1. package/README.md +25 -0
  2. package/app/(shell)/credentials/page.tsx +5 -0
  3. package/app/(shell)/dashboard/page.tsx +14 -0
  4. package/app/(shell)/layout.tsx +11 -0
  5. package/app/(shell)/page.tsx +5 -0
  6. package/app/(shell)/users/page.tsx +5 -0
  7. package/app/(shell)/workflows/[workflowId]/page.tsx +19 -0
  8. package/app/(shell)/workflows/page.tsx +5 -0
  9. package/app/api/[[...path]]/route.ts +40 -0
  10. package/app/api/auth/[...nextauth]/route.ts +3 -0
  11. package/app/globals.css +997 -0
  12. package/app/invite/[token]/page.tsx +10 -0
  13. package/app/layout.tsx +65 -0
  14. package/app/login/layout.tsx +25 -0
  15. package/app/login/page.tsx +22 -0
  16. package/components.json +21 -0
  17. package/docs/FORMS.md +46 -0
  18. package/docs/TAILWIND_SHADCN_MIGRATION.md +89 -0
  19. package/eslint.config.mjs +56 -0
  20. package/middleware.ts +29 -0
  21. package/next-env.d.ts +6 -0
  22. package/next.config.ts +34 -0
  23. package/package.json +76 -0
  24. package/postcss.config.mjs +7 -0
  25. package/public/canvas-icons/builtin/openai.svg +5 -0
  26. package/src/api/CodemationApiClient.ts +107 -0
  27. package/src/api/CodemationApiHttpError.ts +17 -0
  28. package/src/auth/CodemationNextAuthConfigResolver.ts +14 -0
  29. package/src/auth/CodemationNextAuthOAuthProviderDescriptorMapper.ts +30 -0
  30. package/src/auth/CodemationNextAuthOAuthProviderSnapshotResolver.ts +17 -0
  31. package/src/auth/CodemationNextAuthProviderCatalog.ts +107 -0
  32. package/src/auth/codemationEdgeAuth.ts +25 -0
  33. package/src/auth/codemationNextAuth.ts +32 -0
  34. package/src/components/Codemation.tsx +6 -0
  35. package/src/components/CodemationDataTable.tsx +37 -0
  36. package/src/components/CodemationDialog.tsx +137 -0
  37. package/src/components/CodemationFormattedDateTime.tsx +46 -0
  38. package/src/components/GoogleColorGIcon.tsx +39 -0
  39. package/src/components/OauthProviderIcon.tsx +33 -0
  40. package/src/components/PasswordStrengthMeter.tsx +59 -0
  41. package/src/components/forms/index.ts +28 -0
  42. package/src/components/json/JsonMonacoEditor.tsx +75 -0
  43. package/src/components/oauthProviderIconData.ts +17 -0
  44. package/src/components/ui/alert.tsx +56 -0
  45. package/src/components/ui/badge.tsx +40 -0
  46. package/src/components/ui/button.tsx +64 -0
  47. package/src/components/ui/card.tsx +70 -0
  48. package/src/components/ui/collapsible.tsx +26 -0
  49. package/src/components/ui/dialog.tsx +137 -0
  50. package/src/components/ui/dropdown-menu.tsx +238 -0
  51. package/src/components/ui/form.tsx +147 -0
  52. package/src/components/ui/input.tsx +19 -0
  53. package/src/components/ui/label.tsx +26 -0
  54. package/src/components/ui/scroll-area.tsx +47 -0
  55. package/src/components/ui/select.tsx +169 -0
  56. package/src/components/ui/separator.tsx +28 -0
  57. package/src/components/ui/switch.tsx +28 -0
  58. package/src/components/ui/table.tsx +72 -0
  59. package/src/components/ui/tabs.tsx +76 -0
  60. package/src/components/ui/textarea.tsx +18 -0
  61. package/src/components/ui/toggle.tsx +41 -0
  62. package/src/features/credentials/components/CredentialConfirmDialog.tsx +58 -0
  63. package/src/features/credentials/components/CredentialDialog.tsx +252 -0
  64. package/src/features/credentials/components/CredentialDialogFeedback.tsx +36 -0
  65. package/src/features/credentials/components/CredentialDialogFieldRows.tsx +257 -0
  66. package/src/features/credentials/components/CredentialDialogFormSections.tsx +230 -0
  67. package/src/features/credentials/components/CredentialEnvFieldStatusRow.tsx +64 -0
  68. package/src/features/credentials/components/CredentialFieldCopyButton.tsx +48 -0
  69. package/src/features/credentials/components/CredentialsScreenHealthBadge.tsx +21 -0
  70. package/src/features/credentials/components/CredentialsScreenInstancesTable.tsx +108 -0
  71. package/src/features/credentials/components/CredentialsScreenTestFailureAlert.tsx +33 -0
  72. package/src/features/credentials/hooks/useCredentialCreateDialog.ts +33 -0
  73. package/src/features/credentials/hooks/useCredentialDialogSession.ts +616 -0
  74. package/src/features/credentials/hooks/useCredentialsScreen.ts +213 -0
  75. package/src/features/credentials/lib/credentialFieldHelpers.ts +35 -0
  76. package/src/features/credentials/lib/credentialFormTypes.ts +1 -0
  77. package/src/features/credentials/lib/credentialInstanceTestPayloadParser.ts +10 -0
  78. package/src/features/credentials/screens/CredentialsScreen.tsx +187 -0
  79. package/src/features/invite/screens/InviteAcceptScreen.tsx +190 -0
  80. package/src/features/users/components/UsersInviteDialog.tsx +121 -0
  81. package/src/features/users/components/UsersRegenerateDialog.tsx +81 -0
  82. package/src/features/users/components/UsersScreenUserStatusBadge.tsx +19 -0
  83. package/src/features/users/schemas/usersInviteFormSchema.ts +7 -0
  84. package/src/features/users/screens/UsersScreen.tsx +240 -0
  85. package/src/features/workflows/components/WorkflowListFolderSection.tsx +91 -0
  86. package/src/features/workflows/components/WorkflowListItemCard.tsx +67 -0
  87. package/src/features/workflows/components/WorkflowListRoot.tsx +39 -0
  88. package/src/features/workflows/components/WorkflowsListTree.tsx +28 -0
  89. package/src/features/workflows/components/canvas/CanvasNodeChromeTooltip.tsx +96 -0
  90. package/src/features/workflows/components/canvas/CanvasNodeIconSlot.tsx +25 -0
  91. package/src/features/workflows/components/canvas/VisibleNodeStatusResolver.tsx +84 -0
  92. package/src/features/workflows/components/canvas/WorkflowCanvas.tsx +248 -0
  93. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNode.tsx +182 -0
  94. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAccents.tsx +73 -0
  95. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentBottomSourceHandles.tsx +43 -0
  96. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentLabels.tsx +47 -0
  97. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeCard.tsx +202 -0
  98. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeHandles.tsx +77 -0
  99. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeLabelBelow.tsx +51 -0
  100. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeMainGlyph.tsx +64 -0
  101. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeToolbar.tsx +95 -0
  102. package/src/features/workflows/components/canvas/WorkflowCanvasLoadingPlaceholder.tsx +69 -0
  103. package/src/features/workflows/components/canvas/WorkflowCanvasNodeIcon.tsx +102 -0
  104. package/src/features/workflows/components/canvas/WorkflowCanvasSimpleIconGlyph.tsx +21 -0
  105. package/src/features/workflows/components/canvas/WorkflowCanvasStraightCountEdge.tsx +33 -0
  106. package/src/features/workflows/components/canvas/WorkflowCanvasStructureSignature.tsx +7 -0
  107. package/src/features/workflows/components/canvas/WorkflowCanvasSymmetricForkEdge.tsx +32 -0
  108. package/src/features/workflows/components/canvas/WorkflowCanvasToolbarIconButton.tsx +95 -0
  109. package/src/features/workflows/components/canvas/lib/WorkflowCanvasBuiltinIconRegistry.ts +26 -0
  110. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeCountResolver.ts +51 -0
  111. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeStyleResolver.ts +35 -0
  112. package/src/features/workflows/components/canvas/lib/WorkflowCanvasLabelLayoutEstimator.ts +42 -0
  113. package/src/features/workflows/components/canvas/lib/WorkflowCanvasOverlapResolver.ts +78 -0
  114. package/src/features/workflows/components/canvas/lib/WorkflowCanvasPortOrderResolver.ts +25 -0
  115. package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts +56 -0
  116. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSiIconRegistry.ts +18 -0
  117. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSymmetricForkPathPlanner.ts +43 -0
  118. package/src/features/workflows/components/canvas/lib/layoutWorkflow.ts +315 -0
  119. package/src/features/workflows/components/canvas/lib/workflowCanvasEdgeGeometry.ts +3 -0
  120. package/src/features/workflows/components/canvas/lib/workflowCanvasEmbeddedStyles.ts +62 -0
  121. package/src/features/workflows/components/canvas/lib/workflowCanvasFlowTypes.ts +10 -0
  122. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeData.ts +41 -0
  123. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeGeometry.ts +99 -0
  124. package/src/features/workflows/components/canvas/workflowCanvasNodeChrome.tsx +46 -0
  125. package/src/features/workflows/components/realtime/RealtimeContext.tsx +14 -0
  126. package/src/features/workflows/components/realtime/WorkflowRealtimeProvider.tsx +15 -0
  127. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingRow.tsx +209 -0
  128. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingsSection.tsx +227 -0
  129. package/src/features/workflows/components/workflowDetail/NodePropertiesConfigSection.tsx +51 -0
  130. package/src/features/workflows/components/workflowDetail/NodePropertiesPanelHeader.tsx +50 -0
  131. package/src/features/workflows/components/workflowDetail/NodePropertiesSlidePanel.tsx +134 -0
  132. package/src/features/workflows/components/workflowDetail/WorkflowActivationErrorDialog.tsx +71 -0
  133. package/src/features/workflows/components/workflowDetail/WorkflowActivationHeaderControl.tsx +64 -0
  134. package/src/features/workflows/components/workflowDetail/WorkflowDetailIcons.tsx +52 -0
  135. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspector.tsx +110 -0
  136. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorDetailBody.tsx +213 -0
  137. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorPanes.tsx +239 -0
  138. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorSidebarResizer.tsx +31 -0
  139. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorTreePanel.tsx +133 -0
  140. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentGroupingPresenter.tsx +31 -0
  141. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentList.tsx +118 -0
  142. package/src/features/workflows/components/workflowDetail/WorkflowInspectorBinaryView.tsx +15 -0
  143. package/src/features/workflows/components/workflowDetail/WorkflowInspectorErrorView.tsx +107 -0
  144. package/src/features/workflows/components/workflowDetail/WorkflowInspectorJsonView.tsx +114 -0
  145. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreePresenter.tsx +132 -0
  146. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreeViewRenderer.tsx +147 -0
  147. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyView.tsx +65 -0
  148. package/src/features/workflows/components/workflowDetail/WorkflowInspectorViews.tsx +5 -0
  149. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryAttachmentRow.tsx +74 -0
  150. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryUploadRow.tsx +69 -0
  151. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorDialog.tsx +254 -0
  152. package/src/features/workflows/components/workflowDetail/WorkflowRunsList.tsx +89 -0
  153. package/src/features/workflows/components/workflowDetail/WorkflowRunsSidebar.tsx +50 -0
  154. package/src/features/workflows/hooks/canvas/useWorkflowCanvasVisibleNodeStatuses.ts +14 -0
  155. package/src/features/workflows/hooks/realtime/realtime.tsx +271 -0
  156. package/src/features/workflows/hooks/realtime/runQueryPolling.ts +34 -0
  157. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeInfrastructure.ts +541 -0
  158. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeShowDisconnectedBadge.ts +9 -0
  159. package/src/features/workflows/hooks/workflowDetail/useWorkflowDetailController.tsx +1300 -0
  160. package/src/features/workflows/lib/realtime/realtimeApi.ts +78 -0
  161. package/src/features/workflows/lib/realtime/realtimeClientBridge.ts +52 -0
  162. package/src/features/workflows/lib/realtime/realtimeDomainTypes.ts +191 -0
  163. package/src/features/workflows/lib/realtime/realtimeQueryKeys.ts +15 -0
  164. package/src/features/workflows/lib/realtime/realtimeRunMutations.ts +167 -0
  165. package/src/features/workflows/lib/realtime/workflowTypes.ts +5 -0
  166. package/src/features/workflows/lib/workflowDetail/PersistedWorkflowSnapshotMapper.ts +205 -0
  167. package/src/features/workflows/lib/workflowDetail/WorkflowActivationHttpErrorFormat.ts +32 -0
  168. package/src/features/workflows/lib/workflowDetail/WorkflowDetailPresenter.ts +1017 -0
  169. package/src/features/workflows/lib/workflowDetail/WorkflowDetailUrlCodec.ts +70 -0
  170. package/src/features/workflows/lib/workflowDetail/workflowDetailTypes.ts +152 -0
  171. package/src/features/workflows/lib/workflowDetailTreeStyles.ts +65 -0
  172. package/src/features/workflows/screens/WorkflowDetailScreen.tsx +236 -0
  173. package/src/features/workflows/screens/WorkflowDetailScreenInspectorPanel.tsx +55 -0
  174. package/src/features/workflows/screens/WorkflowsList.tsx +35 -0
  175. package/src/features/workflows/screens/WorkflowsScreen.tsx +31 -0
  176. package/src/index.ts +1 -0
  177. package/src/lib/utils.ts +6 -0
  178. package/src/middleware/CodemationNextHostMiddlewarePathRules.ts +31 -0
  179. package/src/providers/CodemationSessionProvider.tsx +23 -0
  180. package/src/providers/Providers.tsx +36 -0
  181. package/src/providers/RealtimeBoundary.tsx +17 -0
  182. package/src/providers/WhitelabelProvider.tsx +22 -0
  183. package/src/server/CodemationAuthPrismaClient.ts +21 -0
  184. package/src/server/CodemationNextHost.ts +379 -0
  185. package/src/shell/AppLayout.tsx +141 -0
  186. package/src/shell/AppLayoutNavItems.tsx +129 -0
  187. package/src/shell/AppLayoutPageHeader.tsx +79 -0
  188. package/src/shell/AppLayoutSidebarBrand.tsx +33 -0
  189. package/src/shell/AppMainContent.tsx +17 -0
  190. package/src/shell/AppShellHeaderActions.tsx +12 -0
  191. package/src/shell/AppShellHeaderActionsAuthenticated.tsx +51 -0
  192. package/src/shell/CodemationNextClientShell.tsx +17 -0
  193. package/src/shell/CredentialsSignInRedirectResolver.ts +21 -0
  194. package/src/shell/LoginPageClient.tsx +231 -0
  195. package/src/shell/WorkflowDetailChromeContext.tsx +42 -0
  196. package/src/shell/WorkflowFolderTreeBuilder.ts +62 -0
  197. package/src/shell/WorkflowFolderUi.ts +42 -0
  198. package/src/shell/WorkflowSidebarNavFolder.tsx +112 -0
  199. package/src/shell/WorkflowSidebarNavTree.tsx +68 -0
  200. package/src/shell/appLayoutPageTitle.ts +16 -0
  201. package/src/shell/appLayoutSidebarIcons.tsx +108 -0
  202. package/src/whitelabel/CodemationWhitelabelSnapshot.ts +4 -0
  203. package/src/whitelabel/CodemationWhitelabelSnapshotFactory.ts +18 -0
  204. package/tsconfig.json +40 -0
  205. package/tsconfig.tsbuildinfo +1 -0
  206. package/vitest.config.ts +34 -0
@@ -0,0 +1,254 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+
5
+ import { CodemationDialog } from "@/components/CodemationDialog";
6
+ import { JsonMonacoEditor } from "@/components/json/JsonMonacoEditor";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9
+ import { WorkflowDetailPresenter } from "../../lib/workflowDetail/WorkflowDetailPresenter";
10
+ import type { JsonEditorState, PinBinaryMapsByItemIndex } from "../../lib/workflowDetail/workflowDetailTypes";
11
+ import { WorkflowJsonEditorBinaryAttachmentRow } from "./WorkflowJsonEditorBinaryAttachmentRow";
12
+ import { WorkflowJsonEditorBinaryUploadRow } from "./WorkflowJsonEditorBinaryUploadRow";
13
+
14
+ export function WorkflowJsonEditorDialog(
15
+ args: Readonly<{
16
+ state: JsonEditorState;
17
+ onClose: () => void;
18
+ onSave: (value: string, binaryMaps?: PinBinaryMapsByItemIndex) => void;
19
+ /** Initial tab when `state.mode === "pin-output"` (defaults to `json`). */
20
+ initialEditorTab?: "json" | "binaries";
21
+ }>,
22
+ ) {
23
+ const { state, onClose, onSave, initialEditorTab } = args;
24
+ const [value, setValue] = useState(state.value);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [binaryMaps, setBinaryMaps] = useState<PinBinaryMapsByItemIndex>(() =>
27
+ state.mode === "pin-output" ? state.binaryMapsByItemIndex : [],
28
+ );
29
+ const [uploadError, setUploadError] = useState<string | null>(null);
30
+ const [uploadBusyKey, setUploadBusyKey] = useState<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ setValue(state.value);
34
+ setError(null);
35
+ setUploadError(null);
36
+ if (state.mode === "pin-output") {
37
+ setBinaryMaps(state.binaryMapsByItemIndex);
38
+ }
39
+ }, [state]);
40
+
41
+ const itemCount = useMemo(() => {
42
+ try {
43
+ return WorkflowDetailPresenter.parseEditableItems(value).length;
44
+ } catch {
45
+ return 0;
46
+ }
47
+ }, [value]);
48
+
49
+ const handleJsonChange = useCallback(
50
+ (nextValue: string | undefined) => {
51
+ const next = nextValue ?? "";
52
+ setValue(next);
53
+ if (error) setError(null);
54
+ if (state.mode !== "pin-output") {
55
+ return;
56
+ }
57
+ try {
58
+ const parsed = WorkflowDetailPresenter.parseEditableItems(next);
59
+ setBinaryMaps((prev) => WorkflowDetailPresenter.reindexBinaryMapsForItemCount(prev, parsed.length));
60
+ } catch {
61
+ // Invalid JSON: keep binary maps until the JSON becomes valid again.
62
+ }
63
+ },
64
+ [error, state.mode],
65
+ );
66
+
67
+ const suggestAttachmentName = useCallback((existing: Readonly<Record<string, unknown>>): string => {
68
+ if (!existing.file) return "file";
69
+ let n = 1;
70
+ while (existing[`file_${n}`]) {
71
+ n += 1;
72
+ }
73
+ return `file_${n}`;
74
+ }, []);
75
+
76
+ const handleUploadOrReplace = useCallback(
77
+ async (itemIndex: number, file: File, attachmentName: string) => {
78
+ if (state.mode !== "pin-output") {
79
+ return;
80
+ }
81
+ const key = `${itemIndex}:${attachmentName}`;
82
+ setUploadError(null);
83
+ setUploadBusyKey(key);
84
+ try {
85
+ const attachment = await WorkflowDetailPresenter.uploadOverlayPinnedBinary({
86
+ workflowId: state.workflowId,
87
+ nodeId: state.nodeId,
88
+ itemIndex,
89
+ attachmentName,
90
+ file,
91
+ });
92
+ setBinaryMaps((prev) => {
93
+ const next = prev.map((row) => ({ ...row }));
94
+ while (next.length <= itemIndex) {
95
+ next.push({});
96
+ }
97
+ next[itemIndex] = { ...next[itemIndex], [attachmentName]: attachment };
98
+ return next;
99
+ });
100
+ } catch (cause: unknown) {
101
+ setUploadError(cause instanceof Error ? cause.message : String(cause));
102
+ } finally {
103
+ setUploadBusyKey(null);
104
+ }
105
+ },
106
+ [state],
107
+ );
108
+
109
+ const handleRemove = useCallback((itemIndex: number, attachmentName: string) => {
110
+ setBinaryMaps((prev) => {
111
+ const next = prev.map((row, i) => {
112
+ if (i !== itemIndex) {
113
+ return { ...row };
114
+ }
115
+ const copy = { ...row };
116
+ delete copy[attachmentName];
117
+ return copy;
118
+ });
119
+ return next;
120
+ });
121
+ }, []);
122
+
123
+ const runSave = useCallback(() => {
124
+ try {
125
+ if (state.mode === "pin-output") {
126
+ const normalized = WorkflowDetailPresenter.formatPinOutputJsonForSubmit(value);
127
+ onSave(normalized, binaryMaps);
128
+ return;
129
+ }
130
+ JSON.parse(value);
131
+ onSave(value);
132
+ } catch (cause) {
133
+ setError(cause instanceof Error ? cause.message : String(cause));
134
+ }
135
+ }, [binaryMaps, onSave, state.mode, value]);
136
+
137
+ return (
138
+ <CodemationDialog
139
+ onClose={onClose}
140
+ testId="workflow-json-editor-dialog"
141
+ size="full"
142
+ showCloseButton={false}
143
+ contentClassName="max-h-[min(90vh,800px)] w-[min(960px,100%)]"
144
+ >
145
+ <CodemationDialog.Title className="font-normal">
146
+ <div className="flex items-start justify-between gap-3">
147
+ <div>
148
+ <div className="text-[15px] font-extrabold">{state.title}</div>
149
+ <div className="mt-1 text-xs text-muted-foreground">
150
+ {state.mode === "pin-output"
151
+ ? "Edit a top-level JSON array of output items (one element per item; Binaries uses the same indices). The engine always uses this shape. Save to pin the node output, then use Run on the canvas to continue."
152
+ : "Provide valid JSON. Objects become one item; arrays become multiple items."}
153
+ </div>
154
+ </div>
155
+ <Button type="button" variant="outline" size="sm" className="shrink-0 text-xs font-bold" onClick={onClose}>
156
+ Close
157
+ </Button>
158
+ </div>
159
+ </CodemationDialog.Title>
160
+ <CodemationDialog.Content className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-4 py-3">
161
+ {state.mode === "pin-output" ? (
162
+ <Tabs defaultValue={initialEditorTab ?? "json"} className="flex min-h-0 flex-1 flex-col gap-2">
163
+ <TabsList variant="line" className="w-full shrink-0 justify-start">
164
+ <TabsTrigger value="json" className="text-xs font-bold">
165
+ JSON
166
+ </TabsTrigger>
167
+ <TabsTrigger
168
+ value="binaries"
169
+ className="text-xs font-bold"
170
+ data-testid="workflow-json-editor-binaries-tab"
171
+ >
172
+ Binaries
173
+ </TabsTrigger>
174
+ </TabsList>
175
+ <TabsContent value="json" className="mt-0 min-h-0 flex-1 overflow-hidden data-[state=inactive]:hidden">
176
+ <JsonMonacoEditor path={`${state.mode}.json`} value={value} onChange={handleJsonChange} error={error} />
177
+ </TabsContent>
178
+ <TabsContent value="binaries" className="mt-0 min-h-0 flex-1 overflow-y-auto data-[state=inactive]:hidden">
179
+ <div className="flex flex-col gap-4 pb-2">
180
+ {itemCount === 0 ? (
181
+ <div className="text-xs text-muted-foreground">
182
+ Fix JSON on the JSON tab so at least one item exists.
183
+ </div>
184
+ ) : null}
185
+ {state.mode === "pin-output"
186
+ ? Array.from({ length: itemCount }, (_, itemIndex) => {
187
+ const row = binaryMaps[itemIndex] ?? {};
188
+ const entries = Object.entries(row);
189
+ const workflowId = state.workflowId;
190
+ return (
191
+ <div
192
+ key={itemIndex}
193
+ className="rounded-md border border-border bg-muted/30 p-3"
194
+ data-testid={`workflow-json-editor-binaries-item-${itemIndex}`}
195
+ >
196
+ <div className="text-xs font-extrabold">Item {itemIndex}</div>
197
+ <div className="mt-2 flex flex-col gap-2">
198
+ {entries.length === 0 ? (
199
+ <div className="text-xs text-muted-foreground">No attachments for this item.</div>
200
+ ) : null}
201
+ {entries.map(([name, attachment]) => (
202
+ <WorkflowJsonEditorBinaryAttachmentRow
203
+ key={`${itemIndex}:${name}:${attachment.id}`}
204
+ workflowId={workflowId}
205
+ itemIndex={itemIndex}
206
+ name={name}
207
+ attachment={attachment}
208
+ uploadBusyKey={uploadBusyKey}
209
+ onReplace={(file) => {
210
+ void handleUploadOrReplace(itemIndex, file, name);
211
+ }}
212
+ onRemove={() => {
213
+ handleRemove(itemIndex, name);
214
+ }}
215
+ />
216
+ ))}
217
+ <WorkflowJsonEditorBinaryUploadRow
218
+ itemIndex={itemIndex}
219
+ suggestName={suggestAttachmentName(row)}
220
+ busyKey={uploadBusyKey}
221
+ onUpload={(file, attachmentName) => {
222
+ void handleUploadOrReplace(itemIndex, file, attachmentName);
223
+ }}
224
+ />
225
+ </div>
226
+ </div>
227
+ );
228
+ })
229
+ : null}
230
+ {uploadError ? <div className="text-xs text-destructive">{uploadError}</div> : null}
231
+ </div>
232
+ </TabsContent>
233
+ </Tabs>
234
+ ) : (
235
+ <JsonMonacoEditor path={`${state.mode}.json`} value={value} onChange={handleJsonChange} error={error} />
236
+ )}
237
+ </CodemationDialog.Content>
238
+ <CodemationDialog.Actions>
239
+ <Button type="button" variant="outline" size="sm" className="text-xs font-bold" onClick={onClose}>
240
+ Cancel
241
+ </Button>
242
+ <Button
243
+ type="button"
244
+ data-testid="workflow-json-editor-save"
245
+ size="sm"
246
+ className="text-xs font-extrabold"
247
+ onClick={runSave}
248
+ >
249
+ Save
250
+ </Button>
251
+ </CodemationDialog.Actions>
252
+ </CodemationDialog>
253
+ );
254
+ }
@@ -0,0 +1,89 @@
1
+ import type { RunSummary } from "../../hooks/realtime/realtime";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ import { WorkflowStatusIcon } from "./WorkflowDetailIcons";
7
+
8
+ export function WorkflowRunsList(
9
+ args: Readonly<{
10
+ displayedRuns: ReadonlyArray<RunSummary> | undefined;
11
+ runsError: string | null;
12
+ selectedRunId: string | null;
13
+ formatRunListWhen: (value: string | undefined) => string;
14
+ formatRunListDurationLine: (run: Pick<RunSummary, "startedAt" | "finishedAt" | "status">) => string;
15
+ getExecutionModeLabel: (run: Pick<RunSummary, "executionOptions"> | undefined) => string | null;
16
+ onSelectRun: (runId: string) => void;
17
+ }>,
18
+ ) {
19
+ const {
20
+ displayedRuns,
21
+ formatRunListDurationLine,
22
+ formatRunListWhen,
23
+ getExecutionModeLabel,
24
+ onSelectRun,
25
+ runsError,
26
+ selectedRunId,
27
+ } = args;
28
+
29
+ if (runsError) return <p className="text-sm text-destructive">Failed to load executions: {runsError}</p>;
30
+ if (!displayedRuns) return <p className="text-sm text-muted-foreground">Loading executions…</p>;
31
+ if (displayedRuns.length === 0) return <p className="text-sm text-muted-foreground">No executions yet.</p>;
32
+
33
+ return (
34
+ <ul className="m-0 grid list-none gap-2 p-0">
35
+ {displayedRuns.map((run) => {
36
+ const whenLabel = formatRunListWhen(run.startedAt);
37
+ const durationLine = formatRunListDurationLine(run);
38
+ const modeLabel = getExecutionModeLabel(run);
39
+ const selected = selectedRunId === run.runId;
40
+ return (
41
+ <li key={run.runId}>
42
+ <button
43
+ type="button"
44
+ data-testid={`run-summary-${run.runId}`}
45
+ aria-label={`${run.status}, ${whenLabel}`}
46
+ onClick={() => onSelectRun(run.runId)}
47
+ className={cn(
48
+ "block w-full cursor-pointer border bg-card p-2.5 text-left font-inherit",
49
+ selected ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50",
50
+ )}
51
+ >
52
+ <span className="visually-hidden" data-testid={`run-status-${run.runId}`}>
53
+ {run.status}
54
+ </span>
55
+ <div className="flex items-start gap-2.5">
56
+ <div className="shrink-0 pt-0.5">
57
+ <WorkflowStatusIcon status={run.status} />
58
+ </div>
59
+ <div className="min-w-0 flex-1">
60
+ <div
61
+ data-testid={`run-started-label-${run.runId}`}
62
+ className="text-sm leading-tight font-bold break-words text-foreground"
63
+ >
64
+ {whenLabel}
65
+ </div>
66
+ <div
67
+ data-testid={`run-duration-line-${run.runId}`}
68
+ className="mt-1 text-xs leading-snug font-semibold text-muted-foreground"
69
+ >
70
+ {durationLine}
71
+ </div>
72
+ </div>
73
+ {modeLabel ? (
74
+ <Badge
75
+ variant="outline"
76
+ data-testid={`run-mode-${run.runId}`}
77
+ className="shrink-0 px-1.5 py-0.5 text-[10px] font-extrabold tracking-wide uppercase"
78
+ >
79
+ {modeLabel}
80
+ </Badge>
81
+ ) : null}
82
+ </div>
83
+ </button>
84
+ </li>
85
+ );
86
+ })}
87
+ </ul>
88
+ );
89
+ }
@@ -0,0 +1,50 @@
1
+ import type {
2
+ WorkflowRunsSidebarActions,
3
+ WorkflowRunsSidebarFormatting,
4
+ WorkflowRunsSidebarModel,
5
+ } from "../../lib/workflowDetail/workflowDetailTypes";
6
+
7
+ import { WorkflowRunsList } from "./WorkflowRunsList";
8
+
9
+ /**
10
+ * Left column when the executions pane is open (see WorkflowDetailScreen grid). Must stay in document flow so
11
+ * `grid-cols-[320px_1fr]` reserves space; `position:absolute` would anchor to the wrong containing block and
12
+ * collapse the canvas column.
13
+ */
14
+ export function WorkflowRunsSidebar(
15
+ args: Readonly<{
16
+ model: WorkflowRunsSidebarModel;
17
+ actions: WorkflowRunsSidebarActions;
18
+ formatting: WorkflowRunsSidebarFormatting;
19
+ }>,
20
+ ) {
21
+ const { actions, formatting, model } = args;
22
+ const { formatRunListDurationLine, formatRunListWhen, getExecutionModeLabel } = formatting;
23
+ const { onSelectRun } = actions;
24
+ const { displayedRuns, error, runsError, selectedRunId, workflowError } = model;
25
+
26
+ return (
27
+ <aside
28
+ data-testid="workflow-runs-sidebar"
29
+ className="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden border-r border-border bg-card shadow-[6px_0_18px_rgba(15,23,42,0.06)]"
30
+ >
31
+ {error || workflowError ? (
32
+ <div className="shrink-0 border-b border-border px-3.5 py-2.5 text-sm text-destructive">
33
+ {error ?? workflowError}
34
+ </div>
35
+ ) : null}
36
+
37
+ <div className="min-h-0 flex-1 overflow-auto p-3.5">
38
+ <WorkflowRunsList
39
+ displayedRuns={displayedRuns}
40
+ formatRunListDurationLine={formatRunListDurationLine}
41
+ formatRunListWhen={formatRunListWhen}
42
+ getExecutionModeLabel={getExecutionModeLabel}
43
+ onSelectRun={onSelectRun}
44
+ runsError={runsError}
45
+ selectedRunId={selectedRunId}
46
+ />
47
+ </div>
48
+ </aside>
49
+ );
50
+ }
@@ -0,0 +1,14 @@
1
+ import { useMemo } from "react";
2
+
3
+ import type { ConnectionInvocationRecord, NodeExecutionSnapshot } from "../realtime/realtime";
4
+ import { VisibleNodeStatusResolver } from "../../components/canvas/VisibleNodeStatusResolver";
5
+
6
+ export function useWorkflowCanvasVisibleNodeStatuses(
7
+ nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>,
8
+ connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>,
9
+ ): Readonly<Record<string, NodeExecutionSnapshot["status"] | undefined>> {
10
+ return useMemo(
11
+ () => VisibleNodeStatusResolver.resolveStatuses(nodeSnapshotsByNodeId, connectionInvocations),
12
+ [connectionInvocations, nodeSnapshotsByNodeId],
13
+ );
14
+ }
@@ -0,0 +1,271 @@
1
+ "use client";
2
+
3
+ export type {
4
+ CredentialInstanceDto,
5
+ CredentialInstanceWithSecretsDto,
6
+ WorkflowCredentialHealthDto,
7
+ } from "@codemation/host-src/application/contracts/CredentialContractsRegistry";
8
+ export type { WorkflowDto, WorkflowSummary } from "@codemation/host-src/application/contracts/WorkflowViewContracts";
9
+ export type {
10
+ InviteUserResponseDto,
11
+ UserAccountDto,
12
+ UserAccountStatus,
13
+ } from "@codemation/host-src/application/contracts/userDirectoryContracts.types";
14
+
15
+ export * from "../../lib/realtime/realtimeDomainTypes";
16
+
17
+ export { WorkflowRealtimeProvider } from "../../components/realtime/WorkflowRealtimeProvider";
18
+
19
+ import {
20
+ withInviteUserResponseLoginMethodsDefaults,
21
+ withUserAccountLoginMethodsDefaults,
22
+ type InviteUserResponseDto,
23
+ type UserAccountDto,
24
+ type UserAccountStatus,
25
+ } from "@codemation/host-src/application/contracts/userDirectoryContracts.types";
26
+ import type { WorkflowDto, WorkflowSummary } from "@codemation/host-src/application/contracts/WorkflowViewContracts";
27
+ import { ApiPaths } from "@codemation/host-src/presentation/http/ApiPaths";
28
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
29
+ import { useContext, useEffect, useState } from "react";
30
+ import { codemationApiClient } from "../../../../api/CodemationApiClient";
31
+ import { RealtimeContext } from "../../components/realtime/RealtimeContext";
32
+ import {
33
+ fetchCredentialFieldEnvStatus,
34
+ fetchCredentialInstanceWithSecrets,
35
+ fetchCredentialInstances,
36
+ fetchCredentialTypes,
37
+ fetchRun,
38
+ fetchUserAccounts,
39
+ fetchWorkflow,
40
+ fetchWorkflowCredentialHealth,
41
+ fetchWorkflowDebuggerOverlay,
42
+ fetchWorkflowRuns,
43
+ fetchWorkflows,
44
+ patchWorkflowActivation,
45
+ } from "../../lib/realtime/realtimeApi";
46
+ import { getRealtimeBridge } from "../../lib/realtime/realtimeClientBridge";
47
+ import {
48
+ credentialFieldEnvStatusQueryKey,
49
+ credentialInstanceWithSecretsQueryKey,
50
+ credentialInstancesQueryKey,
51
+ credentialTypesQueryKey,
52
+ runQueryKey,
53
+ userAccountsQueryKey,
54
+ workflowCredentialHealthQueryKey,
55
+ workflowDebuggerOverlayQueryKey,
56
+ workflowDevBuildStateQueryKey,
57
+ workflowQueryKey,
58
+ workflowRunsQueryKey,
59
+ workflowsQueryKey,
60
+ } from "../../lib/realtime/realtimeQueryKeys";
61
+ import type { PersistedRunState, WorkflowDevBuildState } from "../../lib/realtime/realtimeDomainTypes";
62
+ import { resolveFetchedRunState, resolveRunPollingIntervalMs } from "./runQueryPolling";
63
+
64
+ export function useWorkflowRealtimeSubscription(workflowId: string | null | undefined): void {
65
+ const [bridgeVersion, setBridgeVersion] = useState(0);
66
+ const retainWorkflowSubscription =
67
+ useContext(RealtimeContext)?.retainWorkflowSubscription ?? getRealtimeBridge().retainWorkflowSubscription;
68
+
69
+ useEffect(() => {
70
+ const bridge = getRealtimeBridge();
71
+ const handleBridgeUpdate = () => {
72
+ setBridgeVersion((current) => current + 1);
73
+ };
74
+ bridge.listeners.add(handleBridgeUpdate);
75
+ return () => {
76
+ bridge.listeners.delete(handleBridgeUpdate);
77
+ };
78
+ }, []);
79
+
80
+ useEffect(() => {
81
+ if (!retainWorkflowSubscription || !workflowId) return;
82
+ return retainWorkflowSubscription(workflowId);
83
+ }, [bridgeVersion, retainWorkflowSubscription, workflowId]);
84
+ }
85
+
86
+ export function useWorkflowRealtimeConnectionState(): boolean {
87
+ return useContext(RealtimeContext)?.isConnected ?? false;
88
+ }
89
+
90
+ export function useWorkflowsQuery() {
91
+ return useWorkflowsQueryWithInitialData();
92
+ }
93
+
94
+ export function useWorkflowsQueryWithInitialData(initialData?: ReadonlyArray<WorkflowSummary>) {
95
+ const queryClient = useQueryClient();
96
+ const query = useQuery({
97
+ queryKey: workflowsQueryKey,
98
+ queryFn: fetchWorkflows,
99
+ initialData,
100
+ });
101
+ useEffect(() => {
102
+ if (!initialData) return;
103
+ queryClient.setQueryData(workflowsQueryKey, initialData);
104
+ }, [initialData, queryClient]);
105
+ return query;
106
+ }
107
+
108
+ export function useWorkflowQuery(workflowId: string, initialData?: WorkflowDto) {
109
+ const queryClient = useQueryClient();
110
+ const query = useQuery({
111
+ queryKey: workflowQueryKey(workflowId),
112
+ queryFn: async () => await fetchWorkflow(workflowId),
113
+ enabled: Boolean(workflowId),
114
+ initialData,
115
+ });
116
+ useEffect(() => {
117
+ if (!workflowId || !initialData) return;
118
+ queryClient.setQueryData(workflowQueryKey(workflowId), initialData);
119
+ }, [initialData, queryClient, workflowId]);
120
+ return query;
121
+ }
122
+
123
+ export function useSetWorkflowActivationMutation(workflowId: string) {
124
+ const queryClient = useQueryClient();
125
+ return useMutation({
126
+ mutationFn: async (active: boolean) => await patchWorkflowActivation(workflowId, active),
127
+ onSuccess: async () => {
128
+ await queryClient.invalidateQueries({ queryKey: workflowQueryKey(workflowId) });
129
+ await queryClient.invalidateQueries({ queryKey: workflowsQueryKey });
130
+ },
131
+ });
132
+ }
133
+
134
+ export function useWorkflowRunsQuery(workflowId: string) {
135
+ return useQuery({
136
+ queryKey: workflowRunsQueryKey(workflowId),
137
+ queryFn: async () => await fetchWorkflowRuns(workflowId),
138
+ enabled: Boolean(workflowId),
139
+ });
140
+ }
141
+
142
+ export function useWorkflowDebuggerOverlayQuery(workflowId: string) {
143
+ return useQuery({
144
+ queryKey: workflowDebuggerOverlayQueryKey(workflowId),
145
+ queryFn: async () => await fetchWorkflowDebuggerOverlay(workflowId),
146
+ enabled: Boolean(workflowId),
147
+ });
148
+ }
149
+
150
+ export function useWorkflowDevBuildStateQuery(workflowId: string) {
151
+ return useQuery({
152
+ queryKey: workflowDevBuildStateQueryKey(workflowId),
153
+ queryFn: async (): Promise<WorkflowDevBuildState> => ({
154
+ state: "idle",
155
+ updatedAt: new Date(0).toISOString(),
156
+ }),
157
+ enabled: false,
158
+ initialData: {
159
+ state: "idle",
160
+ updatedAt: new Date(0).toISOString(),
161
+ } satisfies WorkflowDevBuildState,
162
+ });
163
+ }
164
+ export function useRunQuery(
165
+ runId: string | null | undefined,
166
+ options: Readonly<{ disableFetch?: boolean; pollWhileNonTerminalMs?: number }> = {},
167
+ ) {
168
+ const queryClient = useQueryClient();
169
+ return useQuery({
170
+ queryKey: runId ? runQueryKey(runId) : ["run", "disabled"],
171
+ queryFn: async ({ signal }) => {
172
+ const incoming = await fetchRun(runId!, { signal });
173
+ const previous = queryClient.getQueryData<PersistedRunState>(runQueryKey(runId!));
174
+ return resolveFetchedRunState({ incoming, previous });
175
+ },
176
+ enabled: Boolean(runId) && !options.disableFetch,
177
+ refetchInterval: (query) =>
178
+ resolveRunPollingIntervalMs({
179
+ runState: query.state.data as PersistedRunState | undefined,
180
+ pollWhileNonTerminalMs: options.pollWhileNonTerminalMs,
181
+ }),
182
+ staleTime: 30_000,
183
+ });
184
+ }
185
+
186
+ export function useCredentialTypesQuery() {
187
+ return useQuery({
188
+ queryKey: credentialTypesQueryKey,
189
+ queryFn: fetchCredentialTypes,
190
+ });
191
+ }
192
+
193
+ export function useCredentialFieldEnvStatusQuery() {
194
+ return useQuery({
195
+ queryKey: credentialFieldEnvStatusQueryKey,
196
+ queryFn: fetchCredentialFieldEnvStatus,
197
+ });
198
+ }
199
+
200
+ export function useCredentialInstancesQuery() {
201
+ return useQuery({
202
+ queryKey: credentialInstancesQueryKey,
203
+ queryFn: fetchCredentialInstances,
204
+ });
205
+ }
206
+
207
+ export function useCredentialInstanceWithSecretsQuery(instanceId: string | null | undefined) {
208
+ return useQuery({
209
+ queryKey: instanceId
210
+ ? credentialInstanceWithSecretsQueryKey(instanceId)
211
+ : ["credential-instance-with-secrets", "disabled"],
212
+ queryFn: async () => await fetchCredentialInstanceWithSecrets(instanceId!),
213
+ enabled: Boolean(instanceId),
214
+ });
215
+ }
216
+
217
+ export function useWorkflowCredentialHealthQuery(workflowId: string) {
218
+ return useQuery({
219
+ queryKey: workflowCredentialHealthQueryKey(workflowId),
220
+ queryFn: async () => await fetchWorkflowCredentialHealth(workflowId),
221
+ enabled: Boolean(workflowId),
222
+ });
223
+ }
224
+
225
+ export function useUserAccountsQuery() {
226
+ return useQuery({
227
+ queryKey: userAccountsQueryKey,
228
+ queryFn: fetchUserAccounts,
229
+ });
230
+ }
231
+
232
+ export function useInviteUserMutation() {
233
+ const queryClient = useQueryClient();
234
+ return useMutation({
235
+ mutationFn: async (email: string): Promise<InviteUserResponseDto> => {
236
+ const body = await codemationApiClient.postJson<InviteUserResponseDto>(ApiPaths.userInvites(), { email });
237
+ return withInviteUserResponseLoginMethodsDefaults(body);
238
+ },
239
+ onSuccess: async () => {
240
+ await queryClient.invalidateQueries({ queryKey: userAccountsQueryKey });
241
+ },
242
+ });
243
+ }
244
+
245
+ export function useRegenerateUserInviteMutation() {
246
+ const queryClient = useQueryClient();
247
+ return useMutation({
248
+ mutationFn: async (userId: string): Promise<InviteUserResponseDto> => {
249
+ const body = await codemationApiClient.postJson<InviteUserResponseDto>(ApiPaths.userInviteRegenerate(userId));
250
+ return withInviteUserResponseLoginMethodsDefaults(body);
251
+ },
252
+ onSuccess: async () => {
253
+ await queryClient.invalidateQueries({ queryKey: userAccountsQueryKey });
254
+ },
255
+ });
256
+ }
257
+
258
+ export function useUpdateUserAccountStatusMutation() {
259
+ const queryClient = useQueryClient();
260
+ return useMutation({
261
+ mutationFn: async (args: Readonly<{ userId: string; status: UserAccountStatus }>): Promise<UserAccountDto> => {
262
+ const body = await codemationApiClient.patchJson<UserAccountDto>(ApiPaths.userStatus(args.userId), {
263
+ status: args.status,
264
+ });
265
+ return withUserAccountLoginMethodsDefaults(body);
266
+ },
267
+ onSuccess: async () => {
268
+ await queryClient.invalidateQueries({ queryKey: userAccountsQueryKey });
269
+ },
270
+ });
271
+ }