@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,114 @@
1
+ import JsonView from "@uiw/react-json-view";
2
+ import { githubLightTheme } from "@uiw/react-json-view/githubLight";
3
+ import { useState } from "react";
4
+
5
+ import type { CopyState } from "../../lib/workflowDetail/workflowDetailTypes";
6
+
7
+ export function WorkflowInspectorJsonView(args: Readonly<{ value: unknown; emptyLabel: string }>) {
8
+ const { value, emptyLabel } = args;
9
+ const [collapsedLevel, setCollapsedLevel] = useState<boolean | number>(1);
10
+ const [copyState, setCopyState] = useState<CopyState>("idle");
11
+ const isRenderableJson = value !== null && typeof value === "object";
12
+
13
+ if (value === undefined) {
14
+ return (
15
+ <div data-testid="workflow-inspector-empty-state" style={{ opacity: 0.62, fontSize: 13 }}>
16
+ {emptyLabel}
17
+ </div>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <div style={{ height: "100%", minHeight: 0, display: "grid", gridTemplateRows: "auto 1fr", gap: 10 }}>
23
+ <div
24
+ style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}
25
+ >
26
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
27
+ <button
28
+ onClick={() => setCollapsedLevel(true)}
29
+ style={{
30
+ border: "1px solid #d1d5db",
31
+ background: "white",
32
+ padding: "6px 10px",
33
+ cursor: "pointer",
34
+ fontWeight: 700,
35
+ fontSize: 12,
36
+ }}
37
+ >
38
+ Collapse all
39
+ </button>
40
+ <button
41
+ onClick={() => setCollapsedLevel(false)}
42
+ style={{
43
+ border: "1px solid #d1d5db",
44
+ background: "white",
45
+ padding: "6px 10px",
46
+ cursor: "pointer",
47
+ fontWeight: 700,
48
+ fontSize: 12,
49
+ }}
50
+ >
51
+ Expand all
52
+ </button>
53
+ </div>
54
+ <div data-testid="workflow-inspector-json-copy-hint" style={{ fontSize: 12, opacity: 0.65 }}>
55
+ {copyState === "copied" ? "Copied to clipboard" : "Use the copy icon in the viewer"}
56
+ </div>
57
+ </div>
58
+ <div
59
+ data-testid="workflow-inspector-json-panel"
60
+ style={{
61
+ minWidth: 0,
62
+ overflowX: "hidden",
63
+ overflowY: "auto",
64
+ border: "1px solid #d1d5db",
65
+ background: "#f8fafc",
66
+ padding: 12,
67
+ }}
68
+ >
69
+ {isRenderableJson ? (
70
+ <div style={{ minWidth: 0, overflowWrap: "anywhere", wordBreak: "break-word" }}>
71
+ <JsonView
72
+ value={value as object}
73
+ collapsed={collapsedLevel}
74
+ enableClipboard
75
+ displayDataTypes={false}
76
+ displayObjectSize
77
+ shortenTextAfterLength={0}
78
+ style={{
79
+ ...githubLightTheme,
80
+ backgroundColor: "transparent",
81
+ padding: 0,
82
+ fontSize: 12,
83
+ lineHeight: 1.6,
84
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
85
+ }}
86
+ onCopied={() => {
87
+ setCopyState("copied");
88
+ window.setTimeout(() => setCopyState("idle"), 1500);
89
+ }}
90
+ onExpand={() => {
91
+ if (copyState === "copied") setCopyState("idle");
92
+ }}
93
+ />
94
+ </div>
95
+ ) : (
96
+ <pre
97
+ style={{
98
+ margin: 0,
99
+ whiteSpace: "pre-wrap",
100
+ overflowWrap: "anywhere",
101
+ wordBreak: "break-word",
102
+ fontSize: 12,
103
+ lineHeight: 1.6,
104
+ color: "#111827",
105
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
106
+ }}
107
+ >
108
+ {JSON.stringify(value, null, 2)}
109
+ </pre>
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,132 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import type { PrettyJsonTreeNode } from "../../lib/workflowDetail/workflowDetailTypes";
4
+
5
+ export class WorkflowInspectorPrettyTreePresenter {
6
+ static buildTreeData(value: unknown): ReadonlyArray<PrettyJsonTreeNode> {
7
+ if (value === undefined) {
8
+ return [];
9
+ }
10
+ if (Array.isArray(value)) {
11
+ if (value.length === 0) {
12
+ return [this.createEmptyCollectionNode("value", "Array(0)", "pretty-root")];
13
+ }
14
+ return value.map((entry, index) => this.createNode(`[${index}]`, entry, `pretty-root.${index}`));
15
+ }
16
+ if (this.isRecord(value)) {
17
+ const entries = Object.entries(value);
18
+ if (entries.length === 0) {
19
+ return [this.createEmptyCollectionNode("value", "Object(0)", "pretty-root")];
20
+ }
21
+ return entries.map(([key, entry]) => this.createNode(key, entry, `pretty-root.${key}`));
22
+ }
23
+ return [this.createNode("value", value, "pretty-root.value")];
24
+ }
25
+
26
+ static collectKeys(nodes: ReadonlyArray<PrettyJsonTreeNode>): ReadonlyArray<string> {
27
+ const keys: string[] = [];
28
+ this.collectKeysRecursive(nodes, keys);
29
+ return keys;
30
+ }
31
+
32
+ private static collectKeysRecursive(nodes: ReadonlyArray<PrettyJsonTreeNode>, keys: string[]): void {
33
+ for (const node of nodes) {
34
+ const children = node.children ?? [];
35
+ if (children.length > 0) {
36
+ keys.push(node.key);
37
+ this.collectKeysRecursive(children, keys);
38
+ }
39
+ }
40
+ }
41
+
42
+ private static createNode(label: string, value: unknown, key: string): PrettyJsonTreeNode {
43
+ if (Array.isArray(value)) {
44
+ const children = value.map((entry, index) => this.createNode(`[${index}]`, entry, `${key}.${index}`));
45
+ if (children.length === 0) {
46
+ return {
47
+ key,
48
+ label,
49
+ inlineValue: this.renderInlineValue("[]"),
50
+ isLeaf: true,
51
+ };
52
+ }
53
+ return {
54
+ key,
55
+ label,
56
+ children,
57
+ isLeaf: false,
58
+ };
59
+ }
60
+ if (this.isRecord(value)) {
61
+ const children = Object.entries(value).map(([childKey, childValue]) =>
62
+ this.createNode(childKey, childValue, `${key}.${childKey}`),
63
+ );
64
+ if (children.length === 0) {
65
+ return {
66
+ key,
67
+ label,
68
+ inlineValue: this.renderInlineValue("{}"),
69
+ isLeaf: true,
70
+ };
71
+ }
72
+ return {
73
+ key,
74
+ label,
75
+ children,
76
+ isLeaf: false,
77
+ };
78
+ }
79
+ const multilineValue = typeof value === "string" && value.includes("\n") ? value : undefined;
80
+ return {
81
+ key,
82
+ label,
83
+ inlineValue: multilineValue ? undefined : this.renderInlineValue(value),
84
+ multilineValue,
85
+ isLeaf: true,
86
+ };
87
+ }
88
+
89
+ private static createEmptyCollectionNode(label: string, summary: string, key: string): PrettyJsonTreeNode {
90
+ return {
91
+ key,
92
+ label,
93
+ inlineValue: this.renderInlineValue(summary === "Array(0)" ? "[]" : "{}"),
94
+ isLeaf: true,
95
+ };
96
+ }
97
+
98
+ private static renderInlineValue(value: unknown): ReactNode {
99
+ if (typeof value === "string") {
100
+ return (
101
+ <span style={{ color: "#0f766e", fontSize: 12, lineHeight: 1.5, wordBreak: "break-word" }}>
102
+ {value === "" ? '""' : value}
103
+ </span>
104
+ );
105
+ }
106
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
107
+ return (
108
+ <span
109
+ style={{
110
+ border: "1px solid #d1d5db",
111
+ background: "#ffffff",
112
+ color: "#334155",
113
+ fontSize: 11,
114
+ fontWeight: 700,
115
+ padding: "1px 6px",
116
+ }}
117
+ >
118
+ {String(value)}
119
+ </span>
120
+ );
121
+ }
122
+ return (
123
+ <span style={{ color: "#64748b", fontSize: 12, lineHeight: 1.5 }}>
124
+ {value === undefined ? "undefined" : JSON.stringify(value)}
125
+ </span>
126
+ );
127
+ }
128
+
129
+ private static isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
130
+ return value !== null && typeof value === "object" && !Array.isArray(value);
131
+ }
132
+ }
@@ -0,0 +1,147 @@
1
+ import { ChevronRight } from "lucide-react";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import type { PrettyJsonTreeNode } from "../../lib/workflowDetail/workflowDetailTypes";
6
+
7
+ export class WorkflowInspectorPrettyTreeViewRenderer {
8
+ private static readonly INDENT_PX = 18;
9
+
10
+ static renderNodes(
11
+ nodes: ReadonlyArray<PrettyJsonTreeNode>,
12
+ expandedKeys: ReadonlySet<string>,
13
+ onToggle: (key: string) => void,
14
+ depth = 0,
15
+ ): ReactNode {
16
+ return (
17
+ <ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
18
+ {nodes.map((node) => (
19
+ <li key={node.key} style={{ listStyle: "none", margin: 0, padding: 0 }}>
20
+ {node.isLeaf
21
+ ? this.renderLeafNode(node, depth)
22
+ : this.renderBranchNode(node, expandedKeys, onToggle, depth)}
23
+ </li>
24
+ ))}
25
+ </ul>
26
+ );
27
+ }
28
+
29
+ private static renderBranchNode(
30
+ node: PrettyJsonTreeNode,
31
+ expandedKeys: ReadonlySet<string>,
32
+ onToggle: (key: string) => void,
33
+ depth: number,
34
+ ): ReactNode {
35
+ const isExpanded = expandedKeys.has(node.key);
36
+ const children = node.children ?? [];
37
+ return (
38
+ <div data-testid={`pretty-json-branch-${node.key}`} style={{ display: "grid", gap: 4 }}>
39
+ <div
40
+ data-testid={`pretty-json-row-${node.key}`}
41
+ style={{
42
+ display: "flex",
43
+ alignItems: "center",
44
+ gap: 8,
45
+ minWidth: 0,
46
+ padding: "4px 0",
47
+ paddingLeft: depth * this.INDENT_PX,
48
+ }}
49
+ >
50
+ <button
51
+ type="button"
52
+ data-testid={`pretty-json-toggle-${node.key}`}
53
+ aria-expanded={isExpanded}
54
+ onClick={() => onToggle(node.key)}
55
+ style={{
56
+ border: "none",
57
+ background: "transparent",
58
+ padding: 0,
59
+ margin: 0,
60
+ cursor: "pointer",
61
+ fontSize: 12,
62
+ fontWeight: 800,
63
+ color: "#0f172a",
64
+ textAlign: "left",
65
+ display: "inline-flex",
66
+ alignItems: "center",
67
+ gap: 4,
68
+ }}
69
+ >
70
+ <ChevronRight
71
+ size={13}
72
+ strokeWidth={2.2}
73
+ style={{
74
+ color: "#64748b",
75
+ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
76
+ transition: "transform 120ms ease",
77
+ flex: "0 0 auto",
78
+ }}
79
+ />
80
+ {node.label}
81
+ </button>
82
+ </div>
83
+ {isExpanded && children.length > 0 ? this.renderNodes(children, expandedKeys, onToggle, depth + 1) : null}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ private static renderLeafNode(node: PrettyJsonTreeNode, depth: number): ReactNode {
89
+ return (
90
+ <div
91
+ data-testid={`pretty-json-leaf-${node.key}`}
92
+ style={{
93
+ display: "grid",
94
+ gap: node.multilineValue ? 6 : 0,
95
+ padding: "4px 0",
96
+ }}
97
+ >
98
+ <div
99
+ data-testid={`pretty-json-row-${node.key}`}
100
+ style={{
101
+ display: "flex",
102
+ alignItems: "flex-start",
103
+ gap: 8,
104
+ minWidth: 0,
105
+ flexWrap: "wrap",
106
+ paddingLeft: depth * this.INDENT_PX,
107
+ }}
108
+ >
109
+ <span style={{ fontSize: 12, fontWeight: 800, color: "#0f172a" }}>{node.label}</span>
110
+ {node.multilineValue ? (
111
+ <span
112
+ style={{
113
+ border: "1px solid #bfdbfe",
114
+ background: "#eff6ff",
115
+ color: "#1d4ed8",
116
+ fontSize: 11,
117
+ fontWeight: 700,
118
+ padding: "1px 6px",
119
+ }}
120
+ >
121
+ multiline string
122
+ </span>
123
+ ) : (
124
+ node.inlineValue
125
+ )}
126
+ </div>
127
+ {node.multilineValue ? (
128
+ <div
129
+ data-testid={`pretty-json-multiline-${node.key}`}
130
+ style={{
131
+ whiteSpace: "pre-wrap",
132
+ wordBreak: "break-word",
133
+ border: "1px solid #dbeafe",
134
+ background: "#ffffff",
135
+ color: "#0f172a",
136
+ padding: "8px 10px",
137
+ marginLeft: depth * this.INDENT_PX,
138
+ lineHeight: 1.6,
139
+ }}
140
+ >
141
+ {node.multilineValue}
142
+ </div>
143
+ ) : null}
144
+ </div>
145
+ );
146
+ }
147
+ }
@@ -0,0 +1,65 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ import { WorkflowInspectorPrettyTreePresenter } from "./WorkflowInspectorPrettyTreePresenter";
6
+ import { WorkflowInspectorPrettyTreeViewRenderer } from "./WorkflowInspectorPrettyTreeViewRenderer";
7
+
8
+ export function WorkflowInspectorPrettyView(args: Readonly<{ value: unknown; emptyLabel: string }>) {
9
+ const { value, emptyLabel } = args;
10
+ const treeData = useMemo(() => WorkflowInspectorPrettyTreePresenter.buildTreeData(value), [value]);
11
+ const allExpandedKeys = useMemo(() => WorkflowInspectorPrettyTreePresenter.collectKeys(treeData), [treeData]);
12
+ const [expandedKeys, setExpandedKeys] = useState<ReadonlyArray<string>>([]);
13
+ const expandedKeySet = useMemo(() => new Set(expandedKeys), [expandedKeys]);
14
+
15
+ useEffect(() => {
16
+ setExpandedKeys(allExpandedKeys);
17
+ }, [allExpandedKeys]);
18
+
19
+ if (value === undefined) {
20
+ return (
21
+ <div data-testid="workflow-inspector-empty-state" className="text-sm text-muted-foreground opacity-80">
22
+ {emptyLabel}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ return (
28
+ <div className="grid h-full min-h-0 grid-rows-[auto_1fr] gap-2.5">
29
+ <div className="flex flex-wrap items-center justify-between gap-2.5">
30
+ <div className="flex flex-wrap gap-2">
31
+ <Button
32
+ type="button"
33
+ variant="outline"
34
+ size="sm"
35
+ className="text-xs font-bold"
36
+ onClick={() => setExpandedKeys([])}
37
+ >
38
+ Collapse all
39
+ </Button>
40
+ <Button
41
+ type="button"
42
+ variant="outline"
43
+ size="sm"
44
+ className="text-xs font-bold"
45
+ onClick={() => setExpandedKeys(allExpandedKeys)}
46
+ >
47
+ Expand all
48
+ </Button>
49
+ </div>
50
+ <div data-testid="workflow-inspector-pretty-hint" className="text-xs text-muted-foreground">
51
+ Readable tree view with preserved line breaks
52
+ </div>
53
+ </div>
54
+ <div className="min-w-0 overflow-x-hidden overflow-y-auto border border-border bg-muted/40 p-3">
55
+ <div data-testid="workflow-inspector-pretty-tree">
56
+ {WorkflowInspectorPrettyTreeViewRenderer.renderNodes(treeData, expandedKeySet, (key) => {
57
+ setExpandedKeys((current) =>
58
+ current.includes(key) ? current.filter((entry) => entry !== key) : [...current, key],
59
+ );
60
+ })}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,5 @@
1
+ export { WorkflowInspectorAttachmentList } from "./WorkflowInspectorAttachmentList";
2
+ export { WorkflowInspectorBinaryView } from "./WorkflowInspectorBinaryView";
3
+ export { WorkflowInspectorErrorView } from "./WorkflowInspectorErrorView";
4
+ export { WorkflowInspectorJsonView } from "./WorkflowInspectorJsonView";
5
+ export { WorkflowInspectorPrettyView } from "./WorkflowInspectorPrettyView";
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import type { BinaryAttachment } from "@codemation/core/browser";
4
+ import { ApiPaths } from "@codemation/host-src/presentation/http/ApiPaths";
5
+ import { useRef, type ReactNode } from "react";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+
10
+ export function WorkflowJsonEditorBinaryAttachmentRow(
11
+ args: Readonly<{
12
+ workflowId: string;
13
+ itemIndex: number;
14
+ name: string;
15
+ attachment: BinaryAttachment;
16
+ uploadBusyKey: string | null;
17
+ onReplace: (file: File) => void;
18
+ onRemove: () => void;
19
+ }>,
20
+ ): ReactNode {
21
+ const { workflowId, itemIndex, name, attachment, uploadBusyKey, onReplace, onRemove } = args;
22
+ const replaceInputRef = useRef<HTMLInputElement | null>(null);
23
+ return (
24
+ <div
25
+ className="flex flex-wrap items-center gap-2 rounded border border-border/60 bg-background px-2 py-1.5"
26
+ data-testid={`workflow-json-editor-binary-row-${itemIndex}-${name}`}
27
+ >
28
+ <span className="text-xs font-bold">{name}</span>
29
+ <a
30
+ className="text-xs font-medium text-primary underline"
31
+ href={ApiPaths.workflowOverlayBinaryContent(workflowId, attachment.id)}
32
+ target="_blank"
33
+ rel="noreferrer"
34
+ >
35
+ Open
36
+ </a>
37
+ <Input
38
+ ref={replaceInputRef}
39
+ type="file"
40
+ className="hidden"
41
+ data-testid={`workflow-json-editor-binary-replace-${itemIndex}-${name}`}
42
+ onChange={(event) => {
43
+ const file = event.target.files?.[0];
44
+ event.target.value = "";
45
+ if (file) {
46
+ onReplace(file);
47
+ }
48
+ }}
49
+ />
50
+ <Button
51
+ type="button"
52
+ variant="outline"
53
+ size="sm"
54
+ className="h-7 text-[11px] font-bold"
55
+ disabled={uploadBusyKey !== null}
56
+ onClick={() => {
57
+ replaceInputRef.current?.click();
58
+ }}
59
+ >
60
+ Replace
61
+ </Button>
62
+ <Button
63
+ type="button"
64
+ variant="ghost"
65
+ size="sm"
66
+ className="h-7 text-[11px] font-bold text-destructive"
67
+ data-testid={`workflow-json-editor-binary-remove-${itemIndex}-${name}`}
68
+ onClick={onRemove}
69
+ >
70
+ Remove
71
+ </Button>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, type ReactNode } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+
9
+ export function WorkflowJsonEditorBinaryUploadRow(
10
+ args: Readonly<{
11
+ itemIndex: number;
12
+ suggestName: string;
13
+ busyKey: string | null;
14
+ onUpload: (file: File, attachmentName: string) => void;
15
+ }>,
16
+ ): ReactNode {
17
+ const { itemIndex, suggestName, busyKey, onUpload } = args;
18
+ const [name, setName] = useState(suggestName);
19
+ const uploadInputRef = useRef<HTMLInputElement | null>(null);
20
+ const nameFieldId = `workflow-json-editor-binary-name-${itemIndex}`;
21
+ useEffect(() => {
22
+ setName(suggestName);
23
+ }, [suggestName]);
24
+ const disabled = busyKey !== null || !name.trim();
25
+ return (
26
+ <div className="flex flex-wrap items-end gap-2 border-t border-border/60 pt-2">
27
+ <div className="flex min-w-[120px] flex-1 flex-col gap-1.5">
28
+ <Label htmlFor={nameFieldId} className="text-[11px] font-bold text-muted-foreground">
29
+ Attachment name
30
+ </Label>
31
+ <Input
32
+ id={nameFieldId}
33
+ className="h-8 text-xs"
34
+ value={name}
35
+ onChange={(e) => {
36
+ setName(e.target.value);
37
+ }}
38
+ data-testid={`workflow-json-editor-binary-name-${itemIndex}`}
39
+ />
40
+ </div>
41
+ <Input
42
+ ref={uploadInputRef}
43
+ type="file"
44
+ className="hidden"
45
+ data-testid={`workflow-json-editor-binary-upload-${itemIndex}`}
46
+ disabled={disabled}
47
+ onChange={(event) => {
48
+ const file = event.target.files?.[0];
49
+ event.target.value = "";
50
+ if (file && name.trim()) {
51
+ onUpload(file, name.trim());
52
+ }
53
+ }}
54
+ />
55
+ <Button
56
+ type="button"
57
+ variant="secondary"
58
+ size="sm"
59
+ className="h-8 text-[11px] font-bold"
60
+ disabled={disabled}
61
+ onClick={() => {
62
+ uploadInputRef.current?.click();
63
+ }}
64
+ >
65
+ Upload
66
+ </Button>
67
+ </div>
68
+ );
69
+ }