@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,209 @@
1
+ import type {
2
+ UpsertCredentialBindingRequest,
3
+ WorkflowCredentialHealthSlotDto,
4
+ } from "@codemation/host-src/application/contracts/CredentialContractsRegistry";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "@/components/ui/select";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ import { CanvasNodeChromeTooltip } from "../canvas/CanvasNodeChromeTooltip";
11
+ import { AlertCircle, CheckCircle2, HelpCircle, Link2, Loader2, MinusCircle, Pencil, Plus, Unplug } from "lucide-react";
12
+ import type { ReactNode } from "react";
13
+ import type { CredentialInstanceDto } from "../../hooks/realtime/realtime";
14
+
15
+ const INSTANCE_PLACEHOLDER = "__none__";
16
+ const NEW_CREDENTIAL_VALUE = "__new_credential__";
17
+
18
+ class CredentialSlotHealthPresentation {
19
+ private constructor() {}
20
+
21
+ static icon(status: WorkflowCredentialHealthSlotDto["health"]["status"]): ReactNode {
22
+ const iconClass = "size-4 shrink-0";
23
+ if (status === "healthy") {
24
+ return <CheckCircle2 className={cn(iconClass, "text-emerald-600 dark:text-emerald-400")} aria-hidden />;
25
+ }
26
+ if (status === "failing") {
27
+ return <AlertCircle className={cn(iconClass, "text-destructive")} aria-hidden />;
28
+ }
29
+ if (status === "unbound") {
30
+ return <Unplug className={cn(iconClass, "text-amber-600 dark:text-amber-400")} aria-hidden />;
31
+ }
32
+ if (status === "optional-unbound") {
33
+ return <MinusCircle className={cn(iconClass, "text-muted-foreground")} aria-hidden />;
34
+ }
35
+ return <HelpCircle className={cn(iconClass, "text-muted-foreground")} aria-hidden />;
36
+ }
37
+
38
+ static label(status: WorkflowCredentialHealthSlotDto["health"]["status"]): string {
39
+ if (status === "healthy") {
40
+ return "Credential healthy";
41
+ }
42
+ if (status === "failing") {
43
+ return "Credential check failed";
44
+ }
45
+ if (status === "unbound") {
46
+ return "No credential bound";
47
+ }
48
+ if (status === "optional-unbound") {
49
+ return "Optional slot unbound";
50
+ }
51
+ return "Credential status unknown";
52
+ }
53
+ }
54
+
55
+ export function NodeCredentialBindingRow(
56
+ args: Readonly<{
57
+ slot: WorkflowCredentialHealthSlotDto;
58
+ compatibleInstances: ReadonlyArray<CredentialInstanceDto>;
59
+ /** Full catalog (e.g. bound instance may be resolved when not in the compatible filter). */
60
+ allCredentialInstances: ReadonlyArray<CredentialInstanceDto>;
61
+ selectedInstanceId: string;
62
+ isBinding: boolean;
63
+ onSelectInstance: (instanceId: string) => void;
64
+ onBind: (request: UpsertCredentialBindingRequest) => void;
65
+ onEditCredential: (instance: CredentialInstanceDto) => void;
66
+ onRequestNewCredential: () => void;
67
+ }>,
68
+ ) {
69
+ const {
70
+ allCredentialInstances,
71
+ compatibleInstances,
72
+ isBinding,
73
+ onBind,
74
+ onEditCredential,
75
+ onRequestNewCredential,
76
+ onSelectInstance,
77
+ selectedInstanceId,
78
+ slot,
79
+ } = args;
80
+ const slotTestIdSuffix = `${slot.nodeId}-${slot.requirement.slotKey}`;
81
+ const status = slot.health.status;
82
+ const disabledBind = !selectedInstanceId || isBinding;
83
+ const selectedCredentialInstance =
84
+ selectedInstanceId !== ""
85
+ ? (compatibleInstances.find((i) => i.instanceId === selectedInstanceId) ??
86
+ allCredentialInstances.find((i) => i.instanceId === selectedInstanceId))
87
+ : undefined;
88
+ const canEditCredential = Boolean(selectedCredentialInstance);
89
+ const healthTitle = CredentialSlotHealthPresentation.label(status);
90
+
91
+ return (
92
+ <div data-testid={`node-properties-credential-slot-${slotTestIdSuffix}`} className="flex flex-col gap-2 py-2">
93
+ <div className="min-w-0">
94
+ <span className="text-xs font-bold text-foreground">{slot.requirement.label}</span>
95
+ {slot.health.message ? (
96
+ <div
97
+ className={cn(
98
+ "mt-0.5 line-clamp-2 text-[11px] leading-snug",
99
+ status === "failing" ? "text-destructive" : "text-muted-foreground",
100
+ )}
101
+ >
102
+ {slot.health.message}
103
+ </div>
104
+ ) : null}
105
+ </div>
106
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
107
+ <Select
108
+ value={selectedInstanceId || INSTANCE_PLACEHOLDER}
109
+ onValueChange={(value) => {
110
+ if (value === NEW_CREDENTIAL_VALUE) {
111
+ onRequestNewCredential();
112
+ return;
113
+ }
114
+ onSelectInstance(value === INSTANCE_PLACEHOLDER ? "" : value);
115
+ }}
116
+ >
117
+ <SelectTrigger
118
+ className="h-8 min-h-8 w-full min-w-0 flex-1 sm:max-w-none"
119
+ data-testid={`node-properties-credential-slot-select-${slotTestIdSuffix}`}
120
+ size="sm"
121
+ >
122
+ <span className="flex min-w-0 flex-1 items-center gap-2">
123
+ <CanvasNodeChromeTooltip
124
+ testId={`node-properties-credential-slot-status-tooltip-${slotTestIdSuffix}`}
125
+ ariaLabel={healthTitle}
126
+ tooltip={healthTitle}
127
+ >
128
+ <span
129
+ className="inline-flex shrink-0"
130
+ data-testid={`node-properties-credential-slot-status-${slotTestIdSuffix}`}
131
+ >
132
+ {CredentialSlotHealthPresentation.icon(status)}
133
+ </span>
134
+ </CanvasNodeChromeTooltip>
135
+ <SelectValue placeholder="Select credential…" />
136
+ </span>
137
+ </SelectTrigger>
138
+ <SelectContent>
139
+ <SelectItem value={INSTANCE_PLACEHOLDER}>Select credential…</SelectItem>
140
+ {compatibleInstances.map((instance) => (
141
+ <SelectItem key={instance.instanceId} value={instance.instanceId}>
142
+ {instance.displayName}
143
+ </SelectItem>
144
+ ))}
145
+ <SelectSeparator />
146
+ <SelectItem
147
+ value={NEW_CREDENTIAL_VALUE}
148
+ data-testid={`node-properties-credential-slot-new-${slotTestIdSuffix}`}
149
+ className="font-medium"
150
+ >
151
+ <span className="flex items-center gap-2">
152
+ <Plus className="size-4 shrink-0" aria-hidden />
153
+ <span>New credential</span>
154
+ </span>
155
+ </SelectItem>
156
+ </SelectContent>
157
+ </Select>
158
+ <Button
159
+ type="button"
160
+ variant="outline"
161
+ size="sm"
162
+ className="h-8 shrink-0 px-2.5 text-xs font-bold leading-none"
163
+ data-testid={`node-properties-credential-slot-edit-${slotTestIdSuffix}`}
164
+ disabled={!canEditCredential}
165
+ aria-label={canEditCredential ? undefined : "Select a credential to edit"}
166
+ onClick={() => {
167
+ if (selectedCredentialInstance) {
168
+ onEditCredential(selectedCredentialInstance);
169
+ }
170
+ }}
171
+ >
172
+ <span className="inline-flex items-center gap-1.5">
173
+ <Pencil className="size-4 shrink-0" aria-hidden />
174
+ <span className="leading-none">Edit</span>
175
+ </span>
176
+ </Button>
177
+ <Button
178
+ type="button"
179
+ size="sm"
180
+ data-testid={`node-properties-credential-slot-bind-${slotTestIdSuffix}`}
181
+ disabled={disabledBind}
182
+ className="h-8 shrink-0 px-2.5 text-xs font-bold leading-none"
183
+ onClick={() =>
184
+ onBind({
185
+ workflowId: slot.workflowId,
186
+ nodeId: slot.nodeId,
187
+ slotKey: slot.requirement.slotKey,
188
+ instanceId: selectedInstanceId,
189
+ })
190
+ }
191
+ >
192
+ <span className="inline-flex items-center gap-1.5">
193
+ {isBinding ? (
194
+ <>
195
+ <Loader2 className="size-4 shrink-0 animate-spin" aria-hidden />
196
+ <span className="leading-none">Binding…</span>
197
+ </>
198
+ ) : (
199
+ <>
200
+ <Link2 className="size-4 shrink-0" aria-hidden />
201
+ <span className="leading-none">Bind</span>
202
+ </>
203
+ )}
204
+ </span>
205
+ </Button>
206
+ </div>
207
+ </div>
208
+ );
209
+ }
@@ -0,0 +1,227 @@
1
+ import type { UpsertCredentialBindingRequest } from "@codemation/host-src/application/contracts/CredentialContractsRegistry";
2
+ import { ApiPaths } from "@codemation/host-src/presentation/http/ApiPaths";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { codemationApiClient } from "../../../../api/CodemationApiClient";
6
+ import { CredentialConfirmDialog } from "../../../credentials/components/CredentialConfirmDialog";
7
+ import { CredentialDialog } from "../../../credentials/components/CredentialDialog";
8
+ import { useCredentialCreateDialog } from "../../../credentials/hooks/useCredentialCreateDialog";
9
+ import { useCredentialInstancesQuery, useWorkflowCredentialHealthQuery } from "../../hooks/realtime/realtime";
10
+ import { NodeCredentialBindingRow } from "./NodeCredentialBindingRow";
11
+ import type { WorkflowDiagramNode } from "../../lib/workflowDetail/workflowDetailTypes";
12
+
13
+ export function NodeCredentialBindingsSection(
14
+ args: Readonly<{
15
+ workflowId: string;
16
+ node: WorkflowDiagramNode;
17
+ pendingCredentialEditForNodeId: string | null;
18
+ onConsumedPendingCredentialEdit: () => void;
19
+ }>,
20
+ ) {
21
+ const { node, workflowId, pendingCredentialEditForNodeId, onConsumedPendingCredentialEdit } = args;
22
+ const pendingCreateSlotBindingKeyRef = useRef<string | null>(null);
23
+ const queryClient = useQueryClient();
24
+ const credentialInstancesQuery = useCredentialInstancesQuery();
25
+ const workflowCredentialHealthQuery = useWorkflowCredentialHealthQuery(workflowId);
26
+ const [credentialError, setCredentialError] = useState<string | null>(null);
27
+ const [bindingInstanceIdBySlotKey, setBindingInstanceIdBySlotKey] = useState<Readonly<Record<string, string>>>({});
28
+ const [activeBindingSlotKey, setActiveBindingSlotKey] = useState<string | null>(null);
29
+ const {
30
+ isDialogOpen,
31
+ dialogProps,
32
+ openCreateDialog,
33
+ openEditDialog,
34
+ oauthDisconnectConfirmOpen,
35
+ executeOAuthDisconnect,
36
+ cancelOAuthDisconnect,
37
+ } = useCredentialCreateDialog({
38
+ workflowId,
39
+ onCreated: (instance) => {
40
+ const key = pendingCreateSlotBindingKeyRef.current;
41
+ if (key) {
42
+ setBindingInstanceIdBySlotKey((current) => ({
43
+ ...current,
44
+ [key]: instance.instanceId,
45
+ }));
46
+ pendingCreateSlotBindingKeyRef.current = null;
47
+ }
48
+ },
49
+ });
50
+ const nodeCredentialSlots = useMemo(() => {
51
+ const slots = workflowCredentialHealthQuery.data?.slots ?? [];
52
+ return slots.filter((slot) => slot.nodeId === node.id);
53
+ }, [node.id, workflowCredentialHealthQuery.data]);
54
+
55
+ const pendingCredentialEditHandledRef = useRef(false);
56
+
57
+ useEffect(() => {
58
+ if (pendingCredentialEditForNodeId === null) {
59
+ pendingCredentialEditHandledRef.current = false;
60
+ return;
61
+ }
62
+ if (pendingCredentialEditForNodeId !== node.id) {
63
+ pendingCredentialEditHandledRef.current = false;
64
+ }
65
+ }, [node.id, pendingCredentialEditForNodeId]);
66
+
67
+ useEffect(() => {
68
+ if (pendingCredentialEditForNodeId !== node.id) {
69
+ return;
70
+ }
71
+ if (pendingCredentialEditHandledRef.current) {
72
+ return;
73
+ }
74
+ if (workflowCredentialHealthQuery.isLoading) {
75
+ return;
76
+ }
77
+ const slotsWithInstance = nodeCredentialSlots.filter((slot) => slot.instance?.instanceId);
78
+ if (slotsWithInstance.length === 0) {
79
+ pendingCredentialEditHandledRef.current = true;
80
+ onConsumedPendingCredentialEdit();
81
+ return;
82
+ }
83
+ if (credentialInstancesQuery.isLoading) {
84
+ return;
85
+ }
86
+ const first = slotsWithInstance[0];
87
+ const instanceId = first.instance!.instanceId;
88
+ const full = credentialInstancesQuery.data?.find((instance) => instance.instanceId === instanceId);
89
+ pendingCredentialEditHandledRef.current = true;
90
+ if (full) {
91
+ openEditDialog(full);
92
+ }
93
+ onConsumedPendingCredentialEdit();
94
+ }, [
95
+ credentialInstancesQuery.data,
96
+ credentialInstancesQuery.isLoading,
97
+ node.id,
98
+ nodeCredentialSlots,
99
+ onConsumedPendingCredentialEdit,
100
+ openEditDialog,
101
+ pendingCredentialEditForNodeId,
102
+ workflowCredentialHealthQuery.isLoading,
103
+ ]);
104
+
105
+ useEffect(() => {
106
+ setBindingInstanceIdBySlotKey({});
107
+ setCredentialError(null);
108
+ setActiveBindingSlotKey(null);
109
+ }, [node.id]);
110
+
111
+ const bindCredential = (request: UpsertCredentialBindingRequest) => {
112
+ void (async () => {
113
+ const activeKey = `${request.nodeId}:${request.slotKey}`;
114
+ try {
115
+ setActiveBindingSlotKey(activeKey);
116
+ setCredentialError(null);
117
+ await codemationApiClient.putJson<void>(ApiPaths.credentialBindings(), request);
118
+ await Promise.all([
119
+ queryClient.invalidateQueries({ queryKey: ["workflow-credential-health", workflowId] }),
120
+ queryClient.invalidateQueries({ queryKey: ["credential-instances"] }),
121
+ ]);
122
+ } catch (error) {
123
+ setCredentialError(error instanceof Error ? error.message : String(error));
124
+ } finally {
125
+ setActiveBindingSlotKey(null);
126
+ }
127
+ })();
128
+ };
129
+
130
+ return (
131
+ <section data-testid="node-properties-credential-section" style={{ padding: "10px 12px 14px" }}>
132
+ <div style={{ fontSize: 11, fontWeight: 800, letterSpacing: 0.45, textTransform: "uppercase", opacity: 0.64 }}>
133
+ Credentials
134
+ </div>
135
+ {nodeCredentialSlots.length === 0 ? (
136
+ <div
137
+ style={{
138
+ marginTop: 10,
139
+ padding: 10,
140
+ border: "1px solid #e5e7eb",
141
+ background: "#f8fafc",
142
+ fontSize: 12,
143
+ lineHeight: 1.5,
144
+ color: "#475569",
145
+ }}
146
+ >
147
+ No credential slots declared for this node.
148
+ </div>
149
+ ) : (
150
+ <div
151
+ style={{
152
+ marginTop: 10,
153
+ padding: "0 10px",
154
+ border: "1px solid #e5e7eb",
155
+ background: "#ffffff",
156
+ display: "flex",
157
+ flexDirection: "column",
158
+ gap: 0,
159
+ }}
160
+ >
161
+ {nodeCredentialSlots.map((slot, index) => {
162
+ const compatibleInstances =
163
+ credentialInstancesQuery.data?.filter((instance) =>
164
+ slot.requirement.acceptedTypes.includes(instance.typeId),
165
+ ) ?? [];
166
+ const allInstances = credentialInstancesQuery.data ?? [];
167
+ const bindingKey = `${slot.nodeId}:${slot.requirement.slotKey}`;
168
+ const selectedInstanceId = bindingInstanceIdBySlotKey[bindingKey] ?? slot.instance?.instanceId ?? "";
169
+ return (
170
+ <div key={bindingKey} style={{ borderTop: index > 0 ? "1px solid #f1f5f9" : "none" }}>
171
+ <NodeCredentialBindingRow
172
+ slot={slot}
173
+ compatibleInstances={compatibleInstances}
174
+ allCredentialInstances={allInstances}
175
+ selectedInstanceId={selectedInstanceId}
176
+ isBinding={activeBindingSlotKey === bindingKey}
177
+ onSelectInstance={(instanceId) =>
178
+ setBindingInstanceIdBySlotKey((current) => ({
179
+ ...current,
180
+ [bindingKey]: instanceId,
181
+ }))
182
+ }
183
+ onBind={bindCredential}
184
+ onEditCredential={openEditDialog}
185
+ onRequestNewCredential={() => {
186
+ pendingCreateSlotBindingKeyRef.current = bindingKey;
187
+ const accepted = slot.requirement.acceptedTypes;
188
+ openCreateDialog(accepted.length > 0 ? accepted : undefined);
189
+ }}
190
+ />
191
+ </div>
192
+ );
193
+ })}
194
+ </div>
195
+ )}
196
+ {credentialError ? (
197
+ <div style={{ marginTop: 8, fontSize: 12, color: "#b91c1c", lineHeight: 1.35 }}>{credentialError}</div>
198
+ ) : null}
199
+ {oauthDisconnectConfirmOpen ? (
200
+ <CredentialConfirmDialog
201
+ title="Disconnect OAuth2?"
202
+ testId="credential-oauth-disconnect-confirm-dialog"
203
+ cancelTestId="credential-oauth-disconnect-confirm-cancel"
204
+ confirmTestId="credential-oauth-disconnect-confirm-confirm"
205
+ confirmLabel="Disconnect"
206
+ confirmVariant="primary"
207
+ onCancel={cancelOAuthDisconnect}
208
+ onConfirm={() => void executeOAuthDisconnect()}
209
+ >
210
+ <p className="m-0 text-sm text-muted-foreground">
211
+ This will remove the OAuth connection for this credential. You can reconnect later.
212
+ </p>
213
+ </CredentialConfirmDialog>
214
+ ) : null}
215
+ {isDialogOpen && dialogProps ? (
216
+ <CredentialDialog
217
+ key={dialogProps.editingInstance?.instanceId ?? "create"}
218
+ {...dialogProps}
219
+ onClose={() => {
220
+ pendingCreateSlotBindingKeyRef.current = null;
221
+ dialogProps.onClose();
222
+ }}
223
+ />
224
+ ) : null}
225
+ </section>
226
+ );
227
+ }
@@ -0,0 +1,51 @@
1
+ import type { WorkflowDiagramNode } from "../../lib/workflowDetail/workflowDetailTypes";
2
+
3
+ export function NodePropertiesConfigSection(args: Readonly<{ node: WorkflowDiagramNode }>) {
4
+ const { node } = args;
5
+ return (
6
+ <section
7
+ data-testid="node-properties-config-section"
8
+ style={{ padding: "10px 12px", borderBottom: "1px solid #f1f5f9" }}
9
+ >
10
+ <div style={{ fontSize: 11, fontWeight: 800, letterSpacing: 0.45, textTransform: "uppercase", opacity: 0.64 }}>
11
+ Configuration
12
+ </div>
13
+ <p style={{ margin: "8px 0 0", fontSize: 12, lineHeight: 1.5, color: "#475569" }}>
14
+ TODO: surface real node configuration here later, such as model parameters, tool wiring, trigger settings, and
15
+ execution hints.
16
+ </p>
17
+ <div data-testid="node-properties-policy-section" style={{ marginTop: 10 }}>
18
+ <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 0.4, opacity: 0.72 }}>Execution policies</div>
19
+ <div style={{ marginTop: 6, fontSize: 12, color: "#334155", display: "grid", gap: 6 }}>
20
+ <div data-testid="node-properties-retry-policy-line">
21
+ <span style={{ fontWeight: 700 }}>Retry: </span>
22
+ <span data-testid="node-properties-retry-policy-value">{node.retryPolicySummary ?? "—"}</span>
23
+ </div>
24
+ <div data-testid="node-properties-node-error-handler-line">
25
+ <span style={{ fontWeight: 700 }}>Node error handler: </span>
26
+ <span data-testid="node-properties-node-error-handler-value">
27
+ {node.hasNodeErrorHandler ? "yes" : "no"}
28
+ </span>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div
33
+ style={{
34
+ marginTop: 10,
35
+ padding: 10,
36
+ border: "1px dashed #cbd5e1",
37
+ background: "#f8fafc",
38
+ fontSize: 11,
39
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
40
+ color: "#334155",
41
+ lineHeight: 1.55,
42
+ }}
43
+ >
44
+ <div>{`kind: ${node.kind}`}</div>
45
+ <div>{`type: ${node.type}`}</div>
46
+ {node.role ? <div>{`role: ${node.role}`}</div> : null}
47
+ {node.parentNodeId ? <div>{`parentNodeId: ${node.parentNodeId}`}</div> : null}
48
+ </div>
49
+ </section>
50
+ );
51
+ }
@@ -0,0 +1,50 @@
1
+ import { X } from "lucide-react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ export function NodePropertiesPanelHeader(
6
+ args: Readonly<{
7
+ title: string;
8
+ subtitle?: string;
9
+ onClose: () => void;
10
+ }>,
11
+ ) {
12
+ const { onClose, subtitle, title } = args;
13
+ return (
14
+ <div
15
+ data-testid="node-properties-panel-header"
16
+ className="flex shrink-0 items-start justify-between gap-2.5 border-b border-border bg-muted/40 px-3 pt-3 pb-2.5"
17
+ >
18
+ <div className="min-w-0">
19
+ <div className="text-[11px] font-extrabold tracking-wide text-muted-foreground uppercase opacity-90">
20
+ Node properties
21
+ </div>
22
+ <div
23
+ data-testid="node-properties-panel-title"
24
+ className="mt-1 text-sm leading-tight font-extrabold break-words text-foreground"
25
+ >
26
+ {title}
27
+ </div>
28
+ {subtitle ? (
29
+ <div
30
+ data-testid="node-properties-panel-subtitle"
31
+ className="mt-1 break-all font-mono text-[11px] text-muted-foreground"
32
+ >
33
+ {subtitle}
34
+ </div>
35
+ ) : null}
36
+ </div>
37
+ <Button
38
+ type="button"
39
+ variant="outline"
40
+ size="icon-sm"
41
+ data-testid="node-properties-panel-close"
42
+ aria-label="Close node properties"
43
+ onClick={onClose}
44
+ className="shrink-0"
45
+ >
46
+ <X size={16} strokeWidth={2} />
47
+ </Button>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ import { NodeCredentialBindingsSection } from "./NodeCredentialBindingsSection";
8
+ import { NodePropertiesConfigSection } from "./NodePropertiesConfigSection";
9
+ import { NodePropertiesPanelHeader } from "./NodePropertiesPanelHeader";
10
+ import { WorkflowDetailPresenter } from "../../lib/workflowDetail/WorkflowDetailPresenter";
11
+ import type { WorkflowDiagramNode } from "../../lib/workflowDetail/workflowDetailTypes";
12
+
13
+ const PANEL_WIDTH_STORAGE_KEY = "codemation-node-properties-panel-width-px";
14
+ const DEFAULT_PANEL_WIDTH_PX = 300;
15
+ const MIN_PANEL_WIDTH_PX = 240;
16
+ const MAX_PANEL_WIDTH_PX = 560;
17
+
18
+ function loadStoredPanelWidthPx(): number {
19
+ if (typeof window === "undefined") {
20
+ return DEFAULT_PANEL_WIDTH_PX;
21
+ }
22
+ const raw = localStorage.getItem(PANEL_WIDTH_STORAGE_KEY);
23
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
24
+ if (!Number.isFinite(n)) {
25
+ return DEFAULT_PANEL_WIDTH_PX;
26
+ }
27
+ return Math.min(MAX_PANEL_WIDTH_PX, Math.max(MIN_PANEL_WIDTH_PX, n));
28
+ }
29
+
30
+ /**
31
+ * Overlays the canvas from the right (absolute) so the diagram does not reflow when opened/closed.
32
+ */
33
+ export function NodePropertiesSlidePanel(
34
+ args: Readonly<{
35
+ workflowId: string;
36
+ isOpen: boolean;
37
+ node: WorkflowDiagramNode | undefined;
38
+ onClose: () => void;
39
+ pendingCredentialEditForNodeId: string | null;
40
+ onConsumedPendingCredentialEdit: () => void;
41
+ }>,
42
+ ) {
43
+ const { isOpen, node, onClose, workflowId, pendingCredentialEditForNodeId, onConsumedPendingCredentialEdit } = args;
44
+ const isVisible = isOpen && Boolean(node);
45
+ const [panelWidthPx, setPanelWidthPx] = useState(DEFAULT_PANEL_WIDTH_PX);
46
+ const [isResizing, setIsResizing] = useState(false);
47
+ const panelWidthRef = useRef(DEFAULT_PANEL_WIDTH_PX);
48
+ const resizeStartClientXRef = useRef(0);
49
+ const resizeStartWidthPxRef = useRef(DEFAULT_PANEL_WIDTH_PX);
50
+
51
+ useEffect(() => {
52
+ const loaded = loadStoredPanelWidthPx();
53
+ setPanelWidthPx(loaded);
54
+ panelWidthRef.current = loaded;
55
+ }, []);
56
+
57
+ useEffect(() => {
58
+ panelWidthRef.current = panelWidthPx;
59
+ }, [panelWidthPx]);
60
+
61
+ const handleResizeMouseDown = useCallback((event: ReactMouseEvent): void => {
62
+ event.preventDefault();
63
+ setIsResizing(true);
64
+ resizeStartClientXRef.current = event.clientX;
65
+ resizeStartWidthPxRef.current = panelWidthRef.current;
66
+ const onMove = (moveEvent: MouseEvent): void => {
67
+ const delta = resizeStartClientXRef.current - moveEvent.clientX;
68
+ const next = Math.min(MAX_PANEL_WIDTH_PX, Math.max(MIN_PANEL_WIDTH_PX, resizeStartWidthPxRef.current + delta));
69
+ panelWidthRef.current = next;
70
+ setPanelWidthPx(next);
71
+ };
72
+ const onUp = (): void => {
73
+ setIsResizing(false);
74
+ document.removeEventListener("mousemove", onMove);
75
+ document.removeEventListener("mouseup", onUp);
76
+ document.body.style.cursor = "";
77
+ document.body.style.userSelect = "";
78
+ localStorage.setItem(PANEL_WIDTH_STORAGE_KEY, String(panelWidthRef.current));
79
+ };
80
+ document.body.style.cursor = "col-resize";
81
+ document.body.style.userSelect = "none";
82
+ document.addEventListener("mousemove", onMove);
83
+ document.addEventListener("mouseup", onUp);
84
+ }, []);
85
+
86
+ return (
87
+ <aside
88
+ data-testid="node-properties-slide-panel"
89
+ aria-hidden={!isVisible}
90
+ className={cn(
91
+ "absolute top-0 right-0 bottom-0 z-[8] flex flex-col overflow-hidden bg-card shadow-[-6px_0_18px_rgba(15,23,42,0.06)] transition-transform duration-200 ease-out",
92
+ isVisible
93
+ ? "translate-x-0 border-l border-border"
94
+ : "pointer-events-none translate-x-full border-l border-transparent",
95
+ isResizing && "select-none",
96
+ )}
97
+ style={{
98
+ width: panelWidthPx,
99
+ }}
100
+ >
101
+ {isVisible && node ? (
102
+ <div data-testid="node-properties-panel" className="relative flex h-full min-h-0 w-full min-w-0 flex-col">
103
+ <div
104
+ role="separator"
105
+ aria-orientation="vertical"
106
+ aria-label="Resize properties panel"
107
+ data-testid="node-properties-panel-resize-handle"
108
+ className={cn(
109
+ "absolute top-0 bottom-0 left-0 z-[1] w-1 cursor-col-resize bg-transparent hover:bg-primary/30",
110
+ isResizing && "bg-primary/30",
111
+ )}
112
+ onMouseDown={handleResizeMouseDown}
113
+ />
114
+ <div className="flex h-full min-h-0 w-full min-w-0 flex-col pl-1">
115
+ <NodePropertiesPanelHeader
116
+ title={WorkflowDetailPresenter.getNodeDisplayName(node, node.id)}
117
+ subtitle={node.id}
118
+ onClose={onClose}
119
+ />
120
+ <div className="min-h-0 flex-1 overflow-auto">
121
+ <NodePropertiesConfigSection node={node} />
122
+ <NodeCredentialBindingsSection
123
+ workflowId={workflowId}
124
+ node={node}
125
+ pendingCredentialEditForNodeId={pendingCredentialEditForNodeId}
126
+ onConsumedPendingCredentialEdit={onConsumedPendingCredentialEdit}
127
+ />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ ) : null}
132
+ </aside>
133
+ );
134
+ }