@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,230 @@
1
+ "use client";
2
+
3
+ import type { CredentialTypeDefinition } from "@codemation/core/browser";
4
+ import type { Dispatch, SetStateAction } from "react";
5
+
6
+ import { Eye, EyeOff, LogIn, Plug, RefreshCw, Unplug } from "lucide-react";
7
+
8
+ import { Button } from "@/components/ui/button";
9
+ import { Input } from "@/components/ui/input";
10
+ import { Label } from "@/components/ui/label";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
12
+ import type { CredentialInstanceDto } from "../../workflows/hooks/realtime/realtime";
13
+ import type { FormSourceKind } from "../lib/credentialFormTypes";
14
+ import { CredentialFieldCopyButton } from "./CredentialFieldCopyButton";
15
+
16
+ const TYPE_PLACEHOLDER = "__none__";
17
+
18
+ export type CredentialDialogFormSectionsProps = {
19
+ credentialTypes: ReadonlyArray<CredentialTypeDefinition>;
20
+ typesLoading: boolean;
21
+ typesError: boolean;
22
+ typesEmpty: boolean;
23
+ selectedTypeId: string;
24
+ setSelectedTypeId: (v: string) => void;
25
+ displayName: string;
26
+ setDisplayName: (v: string) => void;
27
+ sourceKind: FormSourceKind;
28
+ setSourceKind: (v: FormSourceKind) => void;
29
+ isEdit: boolean;
30
+ isTypeLocked: boolean;
31
+ canToggleSecrets: boolean;
32
+ showSecrets: boolean;
33
+ setShowSecrets: Dispatch<SetStateAction<boolean>>;
34
+ secretsLoading: boolean;
35
+ isOAuth2Type: boolean;
36
+ oauth2RedirectUri: string;
37
+ isLoadingOauth2RedirectUri: boolean;
38
+ editingInstance: CredentialInstanceDto | null | undefined;
39
+ canSubmit: boolean;
40
+ onConnectOAuth2: () => Promise<void>;
41
+ onDisconnectOAuth2: () => void;
42
+ };
43
+
44
+ export function CredentialDialogFormSections({
45
+ credentialTypes,
46
+ typesLoading,
47
+ typesError,
48
+ typesEmpty,
49
+ selectedTypeId,
50
+ setSelectedTypeId,
51
+ displayName,
52
+ setDisplayName,
53
+ sourceKind,
54
+ setSourceKind,
55
+ isEdit,
56
+ isTypeLocked,
57
+ canToggleSecrets,
58
+ showSecrets,
59
+ setShowSecrets,
60
+ secretsLoading,
61
+ isOAuth2Type,
62
+ oauth2RedirectUri,
63
+ isLoadingOauth2RedirectUri,
64
+ editingInstance,
65
+ canSubmit,
66
+ onConnectOAuth2,
67
+ onDisconnectOAuth2,
68
+ }: CredentialDialogFormSectionsProps) {
69
+ return (
70
+ <>
71
+ <div className="flex flex-col gap-2">
72
+ <Label htmlFor="credential-type-select">Credential type</Label>
73
+ <Select
74
+ value={selectedTypeId || TYPE_PLACEHOLDER}
75
+ onValueChange={(v) => setSelectedTypeId(v === TYPE_PLACEHOLDER ? "" : v)}
76
+ disabled={typesLoading || isTypeLocked}
77
+ >
78
+ <SelectTrigger id="credential-type-select" className="w-full" data-testid="credential-type-select">
79
+ <SelectValue placeholder="Select a credential type" />
80
+ </SelectTrigger>
81
+ <SelectContent>
82
+ <SelectItem value={TYPE_PLACEHOLDER}>Select a credential type</SelectItem>
83
+ {credentialTypes.map((type) => (
84
+ <SelectItem key={type.typeId} value={type.typeId}>
85
+ {type.displayName}
86
+ </SelectItem>
87
+ ))}
88
+ </SelectContent>
89
+ </Select>
90
+ {typesLoading && <span className="text-xs text-muted-foreground">Loading…</span>}
91
+ {typesError && <span className="text-sm text-destructive">Failed to load credential types.</span>}
92
+ {!typesLoading && !typesError && typesEmpty && (
93
+ <span className="text-xs text-muted-foreground">No credential types available.</span>
94
+ )}
95
+ </div>
96
+
97
+ <div className="flex flex-col gap-2">
98
+ <Label htmlFor="credential-display-name">Display name</Label>
99
+ <Input
100
+ id="credential-display-name"
101
+ type="text"
102
+ data-testid="credential-display-name-input"
103
+ value={displayName}
104
+ onChange={(e) => setDisplayName(e.target.value)}
105
+ placeholder="e.g. My Gmail account"
106
+ />
107
+ </div>
108
+
109
+ {!isEdit && (
110
+ <div className="flex flex-col gap-2">
111
+ <Label htmlFor="credential-source-kind">Secret source</Label>
112
+ <Select value={sourceKind} onValueChange={(v) => setSourceKind(v as FormSourceKind)}>
113
+ <SelectTrigger id="credential-source-kind" className="w-full" data-testid="credential-source-kind-select">
114
+ <SelectValue />
115
+ </SelectTrigger>
116
+ <SelectContent>
117
+ <SelectItem value="db">Store secret in database</SelectItem>
118
+ <SelectItem value="env">Load from environment variables</SelectItem>
119
+ </SelectContent>
120
+ </Select>
121
+ </div>
122
+ )}
123
+
124
+ {canToggleSecrets && (
125
+ <div className="flex flex-col gap-2">
126
+ <Button
127
+ type="button"
128
+ variant="outline"
129
+ size="sm"
130
+ className="h-8 w-fit gap-1.5 px-2.5 text-xs font-semibold leading-none"
131
+ onClick={() => setShowSecrets((s) => !s)}
132
+ data-testid="credential-show-secrets-toggle"
133
+ disabled={isEdit && secretsLoading}
134
+ >
135
+ <span className="inline-flex items-center gap-1.5">
136
+ {showSecrets ? (
137
+ <EyeOff className="size-3.5 shrink-0" aria-hidden />
138
+ ) : (
139
+ <Eye className="size-3.5 shrink-0" aria-hidden />
140
+ )}
141
+ <span className="leading-none">{showSecrets ? "Hide" : "Show"} values</span>
142
+ </span>
143
+ </Button>
144
+ {isEdit && secretsLoading && <span className="text-xs text-muted-foreground">Loading credential…</span>}
145
+ </div>
146
+ )}
147
+
148
+ {isOAuth2Type && (
149
+ <div className="flex flex-col gap-2">
150
+ <span className="text-sm font-medium leading-none">OAuth2 connection</span>
151
+ {isLoadingOauth2RedirectUri ? (
152
+ <span className="text-xs text-muted-foreground">Loading redirect URI…</span>
153
+ ) : (
154
+ <>
155
+ <div className="flex flex-wrap items-center gap-2">
156
+ <Input
157
+ className="min-w-0 flex-1"
158
+ data-testid="credential-oauth2-redirect-uri"
159
+ type="text"
160
+ readOnly
161
+ value={oauth2RedirectUri}
162
+ />
163
+ <CredentialFieldCopyButton
164
+ value={oauth2RedirectUri}
165
+ label="Copy URI"
166
+ testId="credential-oauth2-redirect-uri-copy"
167
+ />
168
+ </div>
169
+ <span className="text-xs text-muted-foreground">
170
+ Add this redirect URI to your OAuth client (Google Cloud Console, etc.) before connecting.
171
+ </span>
172
+ </>
173
+ )}
174
+ {isEdit && editingInstance?.oauth2Connection?.status === "connected" && (
175
+ <span className="text-xs text-muted-foreground" data-testid="credential-oauth2-connected-status">
176
+ Connected
177
+ {editingInstance.oauth2Connection.connectedEmail
178
+ ? ` as ${editingInstance.oauth2Connection.connectedEmail}`
179
+ : ""}
180
+ </span>
181
+ )}
182
+ <div className="mt-1 flex flex-wrap gap-2">
183
+ <Button
184
+ type="button"
185
+ variant="default"
186
+ size="sm"
187
+ className="h-8 gap-1.5 px-2.5 text-xs font-semibold leading-none"
188
+ data-testid="credential-oauth2-connect-button"
189
+ onClick={() => void onConnectOAuth2()}
190
+ disabled={!isEdit && !canSubmit}
191
+ >
192
+ <span className="inline-flex items-center gap-1.5">
193
+ {isEdit && editingInstance?.oauth2Connection?.status === "connected" ? (
194
+ <RefreshCw className="size-3.5 shrink-0" aria-hidden />
195
+ ) : isEdit ? (
196
+ <Plug className="size-3.5 shrink-0" aria-hidden />
197
+ ) : (
198
+ <LogIn className="size-3.5 shrink-0" aria-hidden />
199
+ )}
200
+ <span className="leading-none">
201
+ {isEdit
202
+ ? editingInstance?.oauth2Connection?.status === "connected"
203
+ ? "Reconnect"
204
+ : "Connect"
205
+ : "Create and connect"}
206
+ </span>
207
+ </span>
208
+ </Button>
209
+ {isEdit && (
210
+ <Button
211
+ type="button"
212
+ variant="destructive"
213
+ size="sm"
214
+ className="h-8 gap-1.5 px-2.5 text-xs font-semibold leading-none"
215
+ data-testid="credential-oauth2-disconnect-button"
216
+ onClick={() => void onDisconnectOAuth2()}
217
+ disabled={editingInstance?.oauth2Connection?.status !== "connected"}
218
+ >
219
+ <span className="inline-flex items-center gap-1.5">
220
+ <Unplug className="size-3.5 shrink-0" aria-hidden />
221
+ <span className="leading-none">Disconnect</span>
222
+ </span>
223
+ </Button>
224
+ )}
225
+ </div>
226
+ </div>
227
+ )}
228
+ </>
229
+ );
230
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { CanvasNodeChromeTooltip } from "../../workflows/components/canvas/CanvasNodeChromeTooltip";
4
+ import { AlertCircle, CheckCircle2 } from "lucide-react";
5
+
6
+ export function CredentialEnvFieldStatusRow(
7
+ args: Readonly<
8
+ | {
9
+ kind: "managed";
10
+ envVarName: string;
11
+ fieldKey: string;
12
+ }
13
+ | {
14
+ kind: "missing";
15
+ envVarName: string;
16
+ fieldKey: string;
17
+ }
18
+ >,
19
+ ) {
20
+ const { envVarName, fieldKey } = args;
21
+ const mono = <span className="font-mono font-semibold text-foreground">{envVarName}</span>;
22
+
23
+ if (args.kind === "managed") {
24
+ const tooltip =
25
+ "This value is provided by the host environment variable. It overrides anything stored in the database for this field.";
26
+ return (
27
+ <div
28
+ className="flex items-start gap-2 rounded-md border border-emerald-200/90 bg-emerald-50/90 px-2.5 py-2 text-xs leading-snug text-emerald-950 dark:border-emerald-900/60 dark:bg-emerald-950/35 dark:text-emerald-100"
29
+ data-testid={`credential-field-env-managed-${fieldKey}`}
30
+ >
31
+ <CanvasNodeChromeTooltip
32
+ testId={`credential-field-env-managed-icon-${fieldKey}`}
33
+ ariaLabel="Host environment configured"
34
+ tooltip={tooltip}
35
+ >
36
+ <span className="inline-flex shrink-0 text-emerald-600 dark:text-emerald-400">
37
+ <CheckCircle2 className="size-4" aria-hidden />
38
+ </span>
39
+ </CanvasNodeChromeTooltip>
40
+ <span>Managed by env {mono}</span>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ const tooltip =
46
+ "This variable is not set (or is empty) in the host process environment. Enter a value below to store it in the database, or set the variable on the host to centralize secrets.";
47
+ return (
48
+ <div
49
+ className="flex items-start gap-2 rounded-md border border-destructive/25 bg-destructive/5 px-2.5 py-2 text-xs leading-snug text-destructive dark:border-destructive/40 dark:bg-destructive/10"
50
+ data-testid={`credential-field-env-missing-${fieldKey}`}
51
+ >
52
+ <CanvasNodeChromeTooltip
53
+ testId={`credential-field-env-missing-icon-${fieldKey}`}
54
+ ariaLabel="Host environment not set"
55
+ tooltip={tooltip}
56
+ >
57
+ <span className="inline-flex shrink-0 text-destructive">
58
+ <AlertCircle className="size-4" aria-hidden />
59
+ </span>
60
+ </CanvasNodeChromeTooltip>
61
+ <span>Not set in host env: {mono}</span>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { ClipboardCopy } from "lucide-react";
5
+ import { useCallback, useState } from "react";
6
+
7
+ export function CredentialFieldCopyButton(
8
+ args: Readonly<{
9
+ value: string;
10
+ /** Default: Copy */
11
+ label?: string;
12
+ testId: string;
13
+ }>,
14
+ ) {
15
+ const { label = "Copy", testId, value } = args;
16
+ const [copied, setCopied] = useState(false);
17
+ const handleCopy = useCallback(() => {
18
+ if (!value.trim()) {
19
+ return;
20
+ }
21
+ void (async () => {
22
+ try {
23
+ await navigator.clipboard.writeText(value);
24
+ setCopied(true);
25
+ window.setTimeout(() => {
26
+ setCopied(false);
27
+ }, 1600);
28
+ } catch {
29
+ setCopied(false);
30
+ }
31
+ })();
32
+ }, [value]);
33
+
34
+ return (
35
+ <Button
36
+ type="button"
37
+ variant="outline"
38
+ size="sm"
39
+ className="h-7 shrink-0 gap-1.5 px-2 text-xs font-semibold leading-none"
40
+ data-testid={testId}
41
+ disabled={!value.trim()}
42
+ onClick={handleCopy}
43
+ >
44
+ <ClipboardCopy className="size-3.5 shrink-0" aria-hidden />
45
+ {copied ? "Copied" : label}
46
+ </Button>
47
+ );
48
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export function CredentialsScreenHealthBadge({ status }: { status: string }) {
7
+ const statusLower = status.toLowerCase();
8
+ const isHealthy = statusLower === "healthy";
9
+ const isFailing = statusLower === "failing";
10
+ return (
11
+ <Badge
12
+ variant={isFailing ? "destructive" : "outline"}
13
+ className={cn(
14
+ isHealthy && "border-emerald-500/30 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200",
15
+ !isHealthy && !isFailing && "text-muted-foreground",
16
+ )}
17
+ >
18
+ {status}
19
+ </Badge>
20
+ );
21
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ import type { CredentialInstanceDto } from "../../workflows/hooks/realtime/realtime";
9
+ import { CredentialsScreenHealthBadge } from "./CredentialsScreenHealthBadge";
10
+
11
+ export type CredentialsScreenInstancesTableProps = {
12
+ credentialInstances: ReadonlyArray<CredentialInstanceDto>;
13
+ testResult: { instanceId: string; status: string; message?: string } | null;
14
+ activeTestInstanceId: string | null;
15
+ onOpenEdit: (instance: CredentialInstanceDto) => void;
16
+ onTest: (instance: CredentialInstanceDto) => Promise<void>;
17
+ onOpenDelete: (instance: CredentialInstanceDto) => void;
18
+ };
19
+
20
+ export function CredentialsScreenInstancesTable({
21
+ credentialInstances,
22
+ testResult,
23
+ activeTestInstanceId,
24
+ onOpenEdit,
25
+ onTest,
26
+ onOpenDelete,
27
+ }: CredentialsScreenInstancesTableProps) {
28
+ return (
29
+ <Table data-testid="credentials-table">
30
+ <TableHeader>
31
+ <TableRow>
32
+ <TableHead>Name</TableHead>
33
+ <TableHead>Type</TableHead>
34
+ <TableHead>Source</TableHead>
35
+ <TableHead>Status</TableHead>
36
+ <TableHead>Health</TableHead>
37
+ <TableHead>Actions</TableHead>
38
+ </TableRow>
39
+ </TableHeader>
40
+ <TableBody>
41
+ {credentialInstances.map((instance) => (
42
+ <TableRow key={instance.instanceId} data-testid={`credential-instance-row-${instance.instanceId}`}>
43
+ <TableCell>
44
+ <button
45
+ type="button"
46
+ className="cursor-pointer border-none bg-transparent p-0 text-left font-medium text-primary underline-offset-4 hover:underline"
47
+ onClick={() => onOpenEdit(instance)}
48
+ data-testid={`credential-instance-name-${instance.instanceId}`}
49
+ >
50
+ {instance.displayName}
51
+ </button>
52
+ </TableCell>
53
+ <TableCell>
54
+ <span className="text-sm text-muted-foreground">{instance.typeId}</span>
55
+ </TableCell>
56
+ <TableCell>
57
+ <Badge variant="outline" className="text-muted-foreground">
58
+ {instance.sourceKind}
59
+ </Badge>
60
+ </TableCell>
61
+ <TableCell>
62
+ <Badge variant="outline" className="text-muted-foreground">
63
+ {instance.setupStatus}
64
+ </Badge>
65
+ </TableCell>
66
+ <TableCell>
67
+ <CredentialsScreenHealthBadge status={instance.latestHealth?.status ?? "unknown"} />
68
+ </TableCell>
69
+ <TableCell>
70
+ <div className="flex flex-wrap items-center gap-2">
71
+ {testResult?.instanceId === instance.instanceId && (
72
+ <span
73
+ className={cn(
74
+ "text-sm font-medium",
75
+ testResult.status === "healthy" && "text-emerald-700 dark:text-emerald-400",
76
+ testResult.status !== "healthy" && "text-destructive",
77
+ )}
78
+ data-testid={`credential-test-result-${instance.instanceId}`}
79
+ >
80
+ {testResult.status === "healthy" ? "Healthy" : "Failing"}
81
+ </span>
82
+ )}
83
+ <Button
84
+ type="button"
85
+ size="sm"
86
+ data-testid={`credential-instance-test-button-${instance.instanceId}`}
87
+ onClick={() => void onTest(instance)}
88
+ disabled={activeTestInstanceId === instance.instanceId}
89
+ >
90
+ {activeTestInstanceId === instance.instanceId ? "Testing…" : "Test"}
91
+ </Button>
92
+ <Button
93
+ type="button"
94
+ size="sm"
95
+ variant="destructive"
96
+ data-testid={`credential-instance-delete-button-${instance.instanceId}`}
97
+ onClick={() => onOpenDelete(instance)}
98
+ >
99
+ Delete
100
+ </Button>
101
+ </div>
102
+ </TableCell>
103
+ </TableRow>
104
+ ))}
105
+ </TableBody>
106
+ </Table>
107
+ );
108
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/ui/alert";
5
+
6
+ export type CredentialsScreenTestFailureAlertProps = {
7
+ message?: string;
8
+ onDismiss: () => void;
9
+ };
10
+
11
+ export function CredentialsScreenTestFailureAlert({ message, onDismiss }: CredentialsScreenTestFailureAlertProps) {
12
+ return (
13
+ <Alert variant="destructive" role="alert" data-testid="credential-test-failure-alert" className="mb-6 items-start">
14
+ <div className="min-w-0 flex-1">
15
+ <AlertTitle>Credential test failed</AlertTitle>
16
+ <AlertDescription className="text-destructive/90">{message || "Test failed"}</AlertDescription>
17
+ </div>
18
+ <AlertAction>
19
+ <Button
20
+ type="button"
21
+ variant="ghost"
22
+ size="icon-sm"
23
+ className="text-destructive hover:bg-destructive/10 hover:text-destructive"
24
+ onClick={onDismiss}
25
+ aria-label="Dismiss"
26
+ data-testid="credential-test-failure-alert-dismiss"
27
+ >
28
+ ×
29
+ </Button>
30
+ </AlertAction>
31
+ </Alert>
32
+ );
33
+ }
@@ -0,0 +1,33 @@
1
+ import type { CredentialInstanceDto } from "../../workflows/hooks/realtime/realtime";
2
+ import { useCredentialDialogSession } from "./useCredentialDialogSession";
3
+
4
+ /**
5
+ * Create / edit credential dialog for embedding outside the Credentials page (e.g. workflow node properties).
6
+ * Delegates to {@link useCredentialDialogSession} with canvas-specific policies.
7
+ */
8
+ export function useCredentialCreateDialog(
9
+ args: Readonly<{
10
+ workflowId: string;
11
+ onCreated?: (instance: CredentialInstanceDto) => void;
12
+ }>,
13
+ ) {
14
+ const { workflowId, onCreated } = args;
15
+ const session = useCredentialDialogSession({
16
+ workflowId,
17
+ onCredentialCreated: onCreated,
18
+ closeAfterCreatePolicy: "unless_oauth2",
19
+ oauthConnectedPolicy: "close_dialog",
20
+ buildDialogProps: true,
21
+ });
22
+
23
+ return {
24
+ isDialogOpen: session.dialogMode !== null,
25
+ dialogProps: session.dialogProps,
26
+ openCreateDialog: session.openCreateDialog,
27
+ openEditDialog: session.openEditDialog,
28
+ closeDialog: session.closeDialog,
29
+ oauthDisconnectConfirmOpen: session.oauthDisconnectConfirmOpen,
30
+ executeOAuthDisconnect: session.executeOAuthDisconnect,
31
+ cancelOAuthDisconnect: session.cancelOAuthDisconnect,
32
+ };
33
+ }