@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,129 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+
5
+ import { usePathname } from "next/navigation";
6
+
7
+ import type { ReactNode } from "react";
8
+
9
+ import { cn } from "@/lib/utils";
10
+
11
+ import { IconCredentials, IconDashboard, IconUsers, IconWorkflow } from "./appLayoutSidebarIcons";
12
+ import { WorkflowSidebarNavTree } from "./WorkflowSidebarNavTree";
13
+ import { useWorkflowsQuery } from "../features/workflows/hooks/realtime/realtime";
14
+
15
+ export interface AppLayoutNavItemsProps {
16
+ readonly collapsed: boolean;
17
+ }
18
+
19
+ const navLinkClass = (isActive: boolean) =>
20
+ cn(
21
+ "flex items-center gap-3 rounded-sm px-4 py-3 text-sm no-underline transition-colors",
22
+ "text-sidebar-foreground hover:bg-sidebar-accent/80",
23
+ isActive && "bg-sidebar-accent font-medium text-sidebar-primary",
24
+ );
25
+
26
+ const workflowLinkClass = (isActive: boolean) =>
27
+ cn(
28
+ "flex items-center gap-3 rounded-sm px-4 py-2 text-sm no-underline transition-colors",
29
+ "text-sidebar-foreground hover:bg-sidebar-accent/80",
30
+ isActive && "bg-sidebar-accent font-medium text-sidebar-primary",
31
+ );
32
+
33
+ const workflowLinkClassCollapsed = (isActive: boolean) =>
34
+ cn(
35
+ "flex items-center justify-center rounded-sm p-3 text-sidebar-foreground no-underline transition-colors hover:bg-sidebar-accent/80",
36
+ isActive && "bg-sidebar-accent font-medium text-sidebar-primary",
37
+ );
38
+
39
+ export function AppLayoutNavItems({ collapsed }: AppLayoutNavItemsProps): ReactNode {
40
+ const pathname = usePathname();
41
+ const workflowsQuery = useWorkflowsQuery();
42
+ const workflows = workflowsQuery.data ?? [];
43
+
44
+ const navItem = (href: string, label: string, icon: ReactNode, exact?: boolean) => {
45
+ const isActive = exact ? pathname === href : pathname.startsWith(href);
46
+ const content = (
47
+ <Link
48
+ key={href}
49
+ href={href}
50
+ className={navLinkClass(isActive)}
51
+ data-testid={`nav-${label.toLowerCase().replace(/\s+/g, "-")}`}
52
+ aria-label={collapsed ? label : undefined}
53
+ >
54
+ <span className="flex shrink-0 items-center justify-center" aria-hidden>
55
+ {icon}
56
+ </span>
57
+ {!collapsed && <span className="truncate">{label}</span>}
58
+ </Link>
59
+ );
60
+ return collapsed ? (
61
+ <span key={href} className="relative flex overflow-visible">
62
+ {content}
63
+ </span>
64
+ ) : (
65
+ content
66
+ );
67
+ };
68
+
69
+ return (
70
+ <>
71
+ {navItem("/dashboard", "Dashboard", <IconDashboard />, true)}
72
+ {navItem("/credentials", "Credentials", <IconCredentials />)}
73
+ {navItem("/users", "Users", <IconUsers />)}
74
+ {collapsed ? (
75
+ <div className="mt-3 flex flex-col gap-1">
76
+ <span className="relative flex overflow-visible">
77
+ <Link
78
+ href="/workflows"
79
+ className={cn(
80
+ "flex items-center justify-center rounded-sm p-3 text-sidebar-foreground no-underline transition-colors hover:bg-sidebar-accent/80",
81
+ pathname === "/workflows" && "bg-sidebar-accent font-medium text-sidebar-primary",
82
+ )}
83
+ data-testid="nav-workflows"
84
+ aria-label="All workflows"
85
+ >
86
+ <span className="flex shrink-0 opacity-70" aria-hidden>
87
+ <IconWorkflow />
88
+ </span>
89
+ </Link>
90
+ </span>
91
+ {workflowsQuery.isLoading && <span className="px-4 py-2 text-xs text-muted-foreground">…</span>}
92
+ {!workflowsQuery.isLoading && workflows.length > 0 && (
93
+ <WorkflowSidebarNavTree
94
+ workflows={workflows}
95
+ pathname={pathname}
96
+ workflowLinkClass={workflowLinkClassCollapsed}
97
+ collapsed
98
+ />
99
+ )}
100
+ </div>
101
+ ) : (
102
+ <div className="mt-4 flex flex-col gap-1">
103
+ <span className="px-4 py-2 text-[0.6875rem] font-semibold uppercase tracking-wider text-muted-foreground">
104
+ Workflows
105
+ </span>
106
+ <div className="flex flex-col gap-1">
107
+ <Link
108
+ href="/workflows"
109
+ className={workflowLinkClass(pathname === "/workflows")}
110
+ data-testid="nav-workflows"
111
+ >
112
+ <span className="flex shrink-0 opacity-70" aria-hidden>
113
+ <IconWorkflow />
114
+ </span>
115
+ <span className="truncate text-sm">All workflows</span>
116
+ </Link>
117
+ {workflowsQuery.isLoading && <span className="px-4 py-2 text-xs text-muted-foreground">Loading…</span>}
118
+ {!workflowsQuery.isLoading && workflows.length === 0 && (
119
+ <span className="px-4 py-2 text-xs text-muted-foreground">No workflows</span>
120
+ )}
121
+ {!workflowsQuery.isLoading && workflows.length > 0 && (
122
+ <WorkflowSidebarNavTree workflows={workflows} pathname={pathname} workflowLinkClass={workflowLinkClass} />
123
+ )}
124
+ </div>
125
+ </div>
126
+ )}
127
+ </>
128
+ );
129
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+
5
+ import type { ReactNode } from "react";
6
+
7
+ import { AlertCircle } from "lucide-react";
8
+
9
+ import { CanvasNodeChromeTooltip } from "../features/workflows/components/canvas/CanvasNodeChromeTooltip";
10
+ import { WorkflowActivationErrorDialog } from "../features/workflows/components/workflowDetail/WorkflowActivationErrorDialog";
11
+ import { WorkflowActivationHeaderControl } from "../features/workflows/components/workflowDetail/WorkflowActivationHeaderControl";
12
+ import { useWhitelabel } from "../providers/WhitelabelProvider";
13
+ import { getPageTitle } from "./appLayoutPageTitle";
14
+ import { AppShellHeaderActions } from "./AppShellHeaderActions";
15
+ import { useWorkflowDetailChrome } from "./WorkflowDetailChromeContext";
16
+ import { useWorkflowsQuery } from "../features/workflows/hooks/realtime/realtime";
17
+
18
+ export function AppLayoutPageHeader(): ReactNode {
19
+ const pathname = usePathname();
20
+ const { productName } = useWhitelabel();
21
+ const workflowsQuery = useWorkflowsQuery();
22
+ const workflows = workflowsQuery.data ?? [];
23
+ const title = getPageTitle(pathname, workflows, productName);
24
+ const chrome = useWorkflowDetailChrome();
25
+ const isWorkflowDetail = /^\/workflows\/[^/]+$/.test(pathname);
26
+ const showChromeRow = isWorkflowDetail && chrome !== null;
27
+ const credentialLines = chrome?.credentialAttentionSummaryLines ?? [];
28
+ const activationAlertLines = chrome?.workflowActivationAlertLines ?? null;
29
+
30
+ return (
31
+ <header className="flex shrink-0 flex-col border-b border-border bg-card">
32
+ <div className="flex h-14 items-center justify-between gap-6 px-8">
33
+ <div className="flex min-w-0 flex-1 items-center gap-3">
34
+ <h1
35
+ className="m-0 min-w-0 truncate text-xl font-semibold leading-none text-foreground"
36
+ data-testid={isWorkflowDetail ? "workflow-detail-workflow-title" : undefined}
37
+ >
38
+ {title}
39
+ </h1>
40
+ {showChromeRow && credentialLines.length > 0 ? (
41
+ <CanvasNodeChromeTooltip
42
+ testId="workflow-credential-attention-indicator"
43
+ ariaLabel="Workflow credential issues"
44
+ tooltip={credentialLines.join("\n")}
45
+ >
46
+ <span
47
+ data-testid="workflow-credential-attention-icon"
48
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-amber-300 bg-amber-50 text-amber-900 shadow-sm"
49
+ >
50
+ <AlertCircle size={16} strokeWidth={2.2} />
51
+ </span>
52
+ </CanvasNodeChromeTooltip>
53
+ ) : null}
54
+ {showChromeRow && chrome?.isLiveWorkflowView ? (
55
+ <WorkflowActivationHeaderControl
56
+ variant="shell"
57
+ showErrorAlert={false}
58
+ active={chrome.workflowIsActive}
59
+ pending={chrome.isWorkflowActivationPending}
60
+ onActiveChange={chrome.setWorkflowActive}
61
+ alertLines={chrome.workflowActivationAlertLines}
62
+ onDismissAlert={chrome.dismissWorkflowActivationAlert}
63
+ />
64
+ ) : null}
65
+ </div>
66
+ <AppShellHeaderActions />
67
+ </div>
68
+ {showChromeRow && chrome && activationAlertLines && activationAlertLines.length > 0 ? (
69
+ <div data-testid="workflow-activation-shell-error">
70
+ <WorkflowActivationErrorDialog
71
+ open
72
+ alertLines={activationAlertLines}
73
+ onDismiss={chrome.dismissWorkflowActivationAlert}
74
+ />
75
+ </div>
76
+ ) : null}
77
+ </header>
78
+ );
79
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { ReactNode } from "react";
5
+
6
+ import { useWhitelabel } from "../providers/WhitelabelProvider";
7
+
8
+ export function AppLayoutSidebarBrand(args: Readonly<{ collapsed: boolean }>): ReactNode {
9
+ const { productName, logoUrl } = useWhitelabel();
10
+ return (
11
+ <Link
12
+ href="/"
13
+ className="flex min-w-0 max-w-full items-center gap-2 text-lg font-semibold text-sidebar-foreground no-underline hover:text-primary"
14
+ data-testid="sidebar-brand"
15
+ >
16
+ {logoUrl !== null ? (
17
+ <img
18
+ src={logoUrl}
19
+ alt=""
20
+ width={32}
21
+ height={32}
22
+ className={args.collapsed ? "size-8 shrink-0 object-contain" : "size-8 shrink-0 object-contain"}
23
+ data-testid="sidebar-whitelabel-logo"
24
+ />
25
+ ) : null}
26
+ {!args.collapsed ? (
27
+ <span className="min-w-0 truncate" data-testid="sidebar-whitelabel-product-name">
28
+ {productName}
29
+ </span>
30
+ ) : null}
31
+ </Link>
32
+ );
33
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+
5
+ import type { ReactNode } from "react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ export function AppMainContent(args: Readonly<{ children: ReactNode }>): ReactNode {
10
+ const pathname = usePathname();
11
+ const isWorkflowDetail = /^\/workflows\/[^/]+$/.test(pathname);
12
+ return (
13
+ <div className={cn("min-h-0 flex-1 overflow-auto p-8", isWorkflowDetail && "overflow-hidden p-0")}>
14
+ {args.children}
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { AppShellHeaderActionsAuthenticated } from "./AppShellHeaderActionsAuthenticated";
6
+
7
+ export function AppShellHeaderActions(): ReactNode {
8
+ if (process.env.NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH === "true") {
9
+ return null;
10
+ }
11
+ return <AppShellHeaderActionsAuthenticated />;
12
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { signOut, useSession } from "next-auth/react";
4
+
5
+ import { useState, type ReactNode } from "react";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+
9
+ export function AppShellHeaderActionsAuthenticated(): ReactNode {
10
+ const { data: session, status } = useSession();
11
+ const [isSigningOut, setIsSigningOut] = useState(false);
12
+
13
+ if (status === "loading") {
14
+ return (
15
+ <div
16
+ className="flex shrink-0 items-center gap-4"
17
+ data-testid="header-session-loading"
18
+ aria-busy="true"
19
+ aria-label="Loading session"
20
+ />
21
+ );
22
+ }
23
+
24
+ const email = session?.user?.email;
25
+ if (!email) {
26
+ return null;
27
+ }
28
+
29
+ const handleSignOut = (): void => {
30
+ setIsSigningOut(true);
31
+ void signOut({ callbackUrl: "/login" });
32
+ };
33
+
34
+ return (
35
+ <div className="flex shrink-0 items-center gap-4">
36
+ <span className="max-w-56 truncate text-xs text-muted-foreground" data-testid="header-user-email">
37
+ {email}
38
+ </span>
39
+ <Button
40
+ type="button"
41
+ variant="outline"
42
+ size="sm"
43
+ data-testid="header-logout"
44
+ disabled={isSigningOut}
45
+ onClick={handleSignOut}
46
+ >
47
+ {isSigningOut ? "Signing out…" : "Log out"}
48
+ </Button>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import "@xyflow/react/dist/style.css";
4
+ import "rc-tree/assets/index.css";
5
+
6
+ import { Component, type ReactNode } from "react";
7
+ import { Providers } from "../providers/Providers";
8
+
9
+ export interface CodemationNextClientShellProps {
10
+ readonly children: ReactNode;
11
+ }
12
+
13
+ export class CodemationNextClientShell extends Component<CodemationNextClientShellProps> {
14
+ render(): ReactNode {
15
+ return <Providers websocketPort={process.env.NEXT_PUBLIC_CODEMATION_WS_PORT}>{this.props.children}</Providers>;
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ import type { SignInResponse } from "next-auth/react";
2
+
3
+ /**
4
+ * Maps NextAuth `signIn(..., { redirect: false })` outcomes to a browser navigation target.
5
+ * Auth.js may return `ok: true` with an empty or missing `url` on success; callers must fall back to `callbackUrl`.
6
+ */
7
+ export class CredentialsSignInRedirectResolver {
8
+ static resolveRedirectUrl(result: SignInResponse, callbackUrl: string): string | null {
9
+ if (result.error) {
10
+ return null;
11
+ }
12
+ const trimmed = result.url?.trim();
13
+ if (trimmed && trimmed.length > 0) {
14
+ return trimmed;
15
+ }
16
+ if (result.ok) {
17
+ return callbackUrl;
18
+ }
19
+ return null;
20
+ }
21
+ }
@@ -0,0 +1,231 @@
1
+ "use client";
2
+
3
+ import { signIn } from "next-auth/react";
4
+ import { Component, type FormEvent, type ReactNode } from "react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Label } from "@/components/ui/label";
10
+ import { Separator } from "@/components/ui/separator";
11
+
12
+ import { OauthProviderIcon } from "../components/OauthProviderIcon";
13
+ import { CredentialsSignInRedirectResolver } from "./CredentialsSignInRedirectResolver";
14
+
15
+ type LoginPageClientProps = Readonly<{
16
+ callbackUrl: string;
17
+ productName: string;
18
+ logoUrl: string | null;
19
+ oauthProviders: ReadonlyArray<{ id: string; name: string }>;
20
+ }>;
21
+
22
+ type LoginPageClientState = Readonly<{
23
+ email: string;
24
+ password: string;
25
+ error: string | null;
26
+ isSubmitting: boolean;
27
+ oauthSubmittingId: string | null;
28
+ }>;
29
+
30
+ export class LoginPageClient extends Component<LoginPageClientProps, LoginPageClientState> {
31
+ constructor(props: LoginPageClientProps) {
32
+ super(props);
33
+ this.state = {
34
+ email: "",
35
+ password: "",
36
+ error: null,
37
+ isSubmitting: false,
38
+ oauthSubmittingId: null,
39
+ };
40
+ }
41
+
42
+ override render(): ReactNode {
43
+ const { isSubmitting, oauthSubmittingId } = this.state;
44
+ const formBusy = isSubmitting || oauthSubmittingId !== null;
45
+ const { productName, logoUrl } = this.props;
46
+ const titleInitial = productName.trim().length > 0 ? productName.trim().charAt(0).toUpperCase() : "C";
47
+
48
+ return (
49
+ <div
50
+ className="relative flex min-h-screen items-center justify-center bg-gradient-to-br from-muted/40 to-background p-4"
51
+ data-testid="login-page"
52
+ >
53
+ <div
54
+ className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/5 via-transparent to-transparent"
55
+ aria-hidden
56
+ />
57
+ <Card className="relative z-10 w-full max-w-md shadow-lg">
58
+ <CardHeader className="gap-3">
59
+ <div className="flex items-start gap-3">
60
+ {logoUrl !== null ? (
61
+ <img
62
+ src={logoUrl}
63
+ alt=""
64
+ width={40}
65
+ height={40}
66
+ className="size-10 shrink-0 rounded-lg object-contain"
67
+ data-testid="login-whitelabel-logo"
68
+ />
69
+ ) : (
70
+ <span
71
+ className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary text-lg font-bold text-primary-foreground"
72
+ aria-hidden
73
+ data-testid="login-whitelabel-initial"
74
+ >
75
+ {titleInitial}
76
+ </span>
77
+ )}
78
+ <div className="min-w-0 space-y-1">
79
+ <CardTitle className="text-xl">Welcome back</CardTitle>
80
+ <p className="text-base font-semibold text-foreground" data-testid="login-whitelabel-product-name">
81
+ {productName}
82
+ </p>
83
+ <CardDescription>Sign in to run and manage your workflows.</CardDescription>
84
+ </div>
85
+ </div>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <form
89
+ className="flex flex-col gap-4"
90
+ suppressHydrationWarning
91
+ aria-busy={isSubmitting}
92
+ onSubmit={(event: FormEvent) => {
93
+ event.preventDefault();
94
+ void this.submitCredentials();
95
+ }}
96
+ >
97
+ <div className="space-y-2" suppressHydrationWarning>
98
+ <Label htmlFor="codemation-login-email">Email</Label>
99
+ <Input
100
+ id="codemation-login-email"
101
+ type="email"
102
+ name="email"
103
+ autoComplete="username"
104
+ placeholder="you@company.com"
105
+ value={this.state.email}
106
+ onChange={(e) => this.setState({ email: e.target.value })}
107
+ required
108
+ disabled={formBusy}
109
+ suppressHydrationWarning
110
+ data-testid="login-email"
111
+ />
112
+ </div>
113
+ <div className="space-y-2" suppressHydrationWarning>
114
+ <Label htmlFor="codemation-login-password">Password</Label>
115
+ <Input
116
+ id="codemation-login-password"
117
+ type="password"
118
+ name="password"
119
+ autoComplete="current-password"
120
+ placeholder="••••••••"
121
+ value={this.state.password}
122
+ onChange={(e) => this.setState({ password: e.target.value })}
123
+ required
124
+ disabled={formBusy}
125
+ suppressHydrationWarning
126
+ data-testid="login-password"
127
+ />
128
+ </div>
129
+ {this.state.error ? (
130
+ <p className="text-sm text-destructive" data-testid="login-error" role="alert">
131
+ {this.state.error}
132
+ </p>
133
+ ) : null}
134
+ <Button
135
+ type="button"
136
+ className="w-full"
137
+ data-testid="login-submit"
138
+ disabled={formBusy}
139
+ onClick={() => {
140
+ void this.submitCredentials();
141
+ }}
142
+ >
143
+ {isSubmitting ? (
144
+ <span
145
+ className="mr-2 inline-block size-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
146
+ aria-hidden
147
+ data-testid="login-submit-spinner"
148
+ />
149
+ ) : null}
150
+ {isSubmitting ? "Signing in…" : "Sign in"}
151
+ </Button>
152
+ </form>
153
+ {this.props.oauthProviders.length > 0 ? (
154
+ <div className="mt-6 space-y-4">
155
+ <div className="flex items-center gap-3">
156
+ <Separator className="flex-1" />
157
+ <span className="text-xs text-muted-foreground">Or</span>
158
+ <Separator className="flex-1" />
159
+ </div>
160
+ <section className="space-y-3" aria-label="OAuth sign-in">
161
+ <p className="text-center text-xs text-muted-foreground">Continue with a connected account</p>
162
+ <div className="flex flex-col gap-2">
163
+ {this.props.oauthProviders.map((provider) => {
164
+ const busy = oauthSubmittingId === provider.id;
165
+ return (
166
+ <Button
167
+ key={provider.id}
168
+ type="button"
169
+ variant="outline"
170
+ className="w-full justify-center gap-2"
171
+ data-testid={`login-oauth-${provider.id}`}
172
+ disabled={formBusy}
173
+ onClick={() => this.handleOAuthSignIn(provider.id)}
174
+ >
175
+ <OauthProviderIcon
176
+ providerId={provider.id}
177
+ className="size-4 shrink-0"
178
+ testId={`login-oauth-${provider.id}-icon`}
179
+ />
180
+ {busy ? "Connecting…" : provider.name}
181
+ </Button>
182
+ );
183
+ })}
184
+ </div>
185
+ </section>
186
+ </div>
187
+ ) : null}
188
+ </CardContent>
189
+ <CardFooter className="justify-center border-t bg-transparent pt-0">
190
+ <p className="text-center text-xs text-muted-foreground" data-testid="login-whitelabel-tagline">
191
+ {productName} — workflow automation you own.
192
+ </p>
193
+ </CardFooter>
194
+ </Card>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ private handleOAuthSignIn(providerId: string): void {
200
+ this.setState({ oauthSubmittingId: providerId, error: null });
201
+ void signIn(providerId, { callbackUrl: this.props.callbackUrl });
202
+ }
203
+
204
+ private async submitCredentials(): Promise<void> {
205
+ this.setState({ error: null, isSubmitting: true });
206
+ try {
207
+ const result = await signIn("credentials", {
208
+ redirect: false,
209
+ email: this.state.email,
210
+ password: this.state.password,
211
+ callbackUrl: this.props.callbackUrl,
212
+ });
213
+ if (!result) {
214
+ this.setState({ error: "Something went wrong. Try again.", isSubmitting: false });
215
+ return;
216
+ }
217
+ if (result.error) {
218
+ this.setState({ error: "Invalid email or password.", isSubmitting: false });
219
+ return;
220
+ }
221
+ const target = CredentialsSignInRedirectResolver.resolveRedirectUrl(result, this.props.callbackUrl);
222
+ if (target) {
223
+ window.location.assign(target);
224
+ return;
225
+ }
226
+ this.setState({ isSubmitting: false });
227
+ } catch {
228
+ this.setState({ error: "Something went wrong. Try again.", isSubmitting: false });
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useMemo,
7
+ useState,
8
+ type Dispatch,
9
+ type ReactNode,
10
+ type SetStateAction,
11
+ } from "react";
12
+
13
+ export type WorkflowDetailChromeState = Readonly<{
14
+ isLiveWorkflowView: boolean;
15
+ workflowIsActive: boolean;
16
+ isWorkflowActivationPending: boolean;
17
+ setWorkflowActive: (active: boolean) => void;
18
+ workflowActivationAlertLines: ReadonlyArray<string> | null;
19
+ dismissWorkflowActivationAlert: () => void;
20
+ credentialAttentionSummaryLines: ReadonlyArray<string>;
21
+ }>;
22
+
23
+ type WorkflowDetailChromeContextValue = Readonly<{
24
+ chrome: WorkflowDetailChromeState | null;
25
+ setChrome: Dispatch<SetStateAction<WorkflowDetailChromeState | null>>;
26
+ }>;
27
+
28
+ const WorkflowDetailChromeContext = createContext<WorkflowDetailChromeContextValue | null>(null);
29
+
30
+ export function WorkflowDetailChromeProvider(args: Readonly<{ children: ReactNode }>): React.JSX.Element {
31
+ const [chrome, setChrome] = useState<WorkflowDetailChromeState | null>(null);
32
+ const value = useMemo(() => ({ chrome, setChrome }), [chrome]);
33
+ return <WorkflowDetailChromeContext.Provider value={value}>{args.children}</WorkflowDetailChromeContext.Provider>;
34
+ }
35
+
36
+ export function useWorkflowDetailChrome(): WorkflowDetailChromeState | null {
37
+ return useContext(WorkflowDetailChromeContext)?.chrome ?? null;
38
+ }
39
+
40
+ export function useWorkflowDetailChromeDispatch(): Dispatch<SetStateAction<WorkflowDetailChromeState | null>> | null {
41
+ return useContext(WorkflowDetailChromeContext)?.setChrome ?? null;
42
+ }