@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,58 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { CodemationDialog } from "@/components/CodemationDialog";
7
+
8
+ export type CredentialConfirmVariant = "danger" | "primary";
9
+
10
+ export type CredentialConfirmDialogProps = {
11
+ title: string;
12
+ testId: string;
13
+ cancelTestId: string;
14
+ confirmTestId: string;
15
+ confirmLabel: string;
16
+ confirmVariant: CredentialConfirmVariant;
17
+ onCancel: () => void;
18
+ onConfirm: () => void;
19
+ children: ReactNode;
20
+ };
21
+
22
+ export function CredentialConfirmDialog({
23
+ title,
24
+ testId,
25
+ cancelTestId,
26
+ confirmTestId,
27
+ confirmLabel,
28
+ confirmVariant,
29
+ onCancel,
30
+ onConfirm,
31
+ children,
32
+ }: CredentialConfirmDialogProps) {
33
+ return (
34
+ <CodemationDialog
35
+ onClose={onCancel}
36
+ testId={testId}
37
+ role="alertdialog"
38
+ size="narrow"
39
+ contentClassName="max-h-[min(70vh,480px)]"
40
+ >
41
+ <CodemationDialog.Title>{title}</CodemationDialog.Title>
42
+ <CodemationDialog.Content>{children}</CodemationDialog.Content>
43
+ <CodemationDialog.Actions>
44
+ <Button type="button" variant="outline" data-testid={cancelTestId} onClick={onCancel}>
45
+ Cancel
46
+ </Button>
47
+ <Button
48
+ type="button"
49
+ variant={confirmVariant === "danger" ? "destructive" : "default"}
50
+ data-testid={confirmTestId}
51
+ onClick={onConfirm}
52
+ >
53
+ {confirmLabel}
54
+ </Button>
55
+ </CodemationDialog.Actions>
56
+ </CodemationDialog>
57
+ );
58
+ }
@@ -0,0 +1,252 @@
1
+ "use client";
2
+
3
+ import type { CredentialFieldSchema, CredentialTypeDefinition } from "@codemation/core/browser";
4
+ import { useEffect, useMemo } from "react";
5
+ import type { Dispatch, SetStateAction } from "react";
6
+
7
+ import { FlaskConical, PlusCircle, Save, X } from "lucide-react";
8
+
9
+ import { Button } from "@/components/ui/button";
10
+ import { CodemationDialog } from "@/components/CodemationDialog";
11
+ import type { CredentialInstanceDto } from "../../workflows/hooks/realtime/realtime";
12
+ import type { FormSourceKind } from "../lib/credentialFormTypes";
13
+ import { isCredentialFieldLockedByEnv } from "../lib/credentialFieldHelpers";
14
+ import { CredentialDialogFeedback } from "./CredentialDialogFeedback";
15
+ import { CredentialDialogFieldRows } from "./CredentialDialogFieldRows";
16
+ import { CredentialDialogFormSections } from "./CredentialDialogFormSections";
17
+
18
+ export type CredentialDialogProps = {
19
+ mode: "create" | "edit";
20
+ credentialTypes: ReadonlyArray<CredentialTypeDefinition>;
21
+ typesLoading: boolean;
22
+ typesError: boolean;
23
+ typesEmpty: boolean;
24
+ selectedTypeId: string;
25
+ setSelectedTypeId: (v: string) => void;
26
+ displayName: string;
27
+ setDisplayName: (v: string) => void;
28
+ sourceKind: FormSourceKind;
29
+ setSourceKind: (v: FormSourceKind) => void;
30
+ publicFieldValues: Record<string, string>;
31
+ setPublicFieldValues: Dispatch<SetStateAction<Record<string, string>>>;
32
+ secretFieldValues: Record<string, string>;
33
+ setSecretFieldValues: Dispatch<SetStateAction<Record<string, string>>>;
34
+ envRefValues: Record<string, string>;
35
+ setEnvRefValues: Dispatch<SetStateAction<Record<string, string>>>;
36
+ showSecrets: boolean;
37
+ setShowSecrets: Dispatch<SetStateAction<boolean>>;
38
+ oauth2RedirectUri: string;
39
+ isLoadingOauth2RedirectUri: boolean;
40
+ secretsLoading?: boolean;
41
+ editingInstance: CredentialInstanceDto | null | undefined;
42
+ errorMessage: string | null;
43
+ dialogTestResult: { status: string; message?: string } | null;
44
+ isSubmitting: boolean;
45
+ isDialogTesting: boolean;
46
+ onCreate: () => Promise<void>;
47
+ onUpdate: () => Promise<void>;
48
+ onTest: () => Promise<void>;
49
+ onConnectOAuth2: () => Promise<void>;
50
+ onDisconnectOAuth2: () => void;
51
+ onClose: () => void;
52
+ credentialFieldEnvStatus?: Readonly<Record<string, boolean>>;
53
+ };
54
+
55
+ export function CredentialDialog({
56
+ mode,
57
+ credentialTypes,
58
+ typesLoading,
59
+ typesError,
60
+ typesEmpty,
61
+ selectedTypeId,
62
+ setSelectedTypeId,
63
+ displayName,
64
+ setDisplayName,
65
+ sourceKind,
66
+ setSourceKind,
67
+ publicFieldValues,
68
+ setPublicFieldValues,
69
+ secretFieldValues,
70
+ setSecretFieldValues,
71
+ envRefValues,
72
+ setEnvRefValues,
73
+ showSecrets,
74
+ setShowSecrets,
75
+ oauth2RedirectUri,
76
+ isLoadingOauth2RedirectUri,
77
+ secretsLoading = false,
78
+ editingInstance,
79
+ errorMessage,
80
+ dialogTestResult,
81
+ isSubmitting,
82
+ isDialogTesting,
83
+ onCreate,
84
+ onUpdate,
85
+ onTest,
86
+ onConnectOAuth2,
87
+ onDisconnectOAuth2,
88
+ onClose,
89
+ credentialFieldEnvStatus = {},
90
+ }: CredentialDialogProps) {
91
+ const selectedType = credentialTypes.find((t) => t.typeId === selectedTypeId);
92
+ const activeType =
93
+ mode === "edit" && editingInstance
94
+ ? credentialTypes.find((t) => t.typeId === editingInstance.typeId)
95
+ : selectedType;
96
+ const publicFields = (activeType?.publicFields ?? []) as ReadonlyArray<CredentialFieldSchema>;
97
+ const secretFields = (activeType?.secretFields ?? []) as ReadonlyArray<CredentialFieldSchema>;
98
+ const isOAuth2Type = activeType?.auth?.kind === "oauth2";
99
+ const orderedFields = [
100
+ ...publicFields.map((field, index) => ({
101
+ kind: "public" as const,
102
+ field,
103
+ order: field.order ?? index,
104
+ })),
105
+ ...secretFields.map((field, index) => ({
106
+ kind: "secret" as const,
107
+ field,
108
+ order: field.order ?? publicFields.length + index,
109
+ })),
110
+ ].sort((left, right) => left.order - right.order);
111
+
112
+ const isEdit = mode === "edit";
113
+ const isTypeLocked = isEdit && editingInstance != null;
114
+ const isDbSecretSource = isEdit ? editingInstance?.sourceKind === "db" : sourceKind === "db";
115
+ const hasVisiblePasswordSecretField = useMemo(() => {
116
+ let hasVisible = false;
117
+ for (const field of secretFields) {
118
+ if (field.type !== "password") {
119
+ continue;
120
+ }
121
+ if (isCredentialFieldLockedByEnv(field, credentialFieldEnvStatus)) {
122
+ continue;
123
+ }
124
+ hasVisible = true;
125
+ break;
126
+ }
127
+ return hasVisible;
128
+ }, [secretFields, credentialFieldEnvStatus]);
129
+ const canToggleSecrets = isDbSecretSource && hasVisiblePasswordSecretField;
130
+
131
+ useEffect(() => {
132
+ if (isEdit && editingInstance) {
133
+ setDisplayName(editingInstance.displayName);
134
+ }
135
+ }, [isEdit, editingInstance?.instanceId, editingInstance?.displayName, setDisplayName]);
136
+
137
+ const canSubmit =
138
+ !isSubmitting &&
139
+ displayName.trim().length > 0 &&
140
+ !publicFields.some(
141
+ (field) =>
142
+ field.required &&
143
+ !isCredentialFieldLockedByEnv(field, credentialFieldEnvStatus) &&
144
+ !(publicFieldValues[field.key] ?? "").trim(),
145
+ ) &&
146
+ (isEdit
147
+ ? true
148
+ : Boolean(selectedTypeId) &&
149
+ (sourceKind === "db"
150
+ ? !secretFields.some(
151
+ (f) =>
152
+ f.required &&
153
+ !isCredentialFieldLockedByEnv(f, credentialFieldEnvStatus) &&
154
+ !(secretFieldValues[f.key] ?? "").trim(),
155
+ )
156
+ : !secretFields.some(
157
+ (f) =>
158
+ f.required &&
159
+ !isCredentialFieldLockedByEnv(f, credentialFieldEnvStatus) &&
160
+ !(envRefValues[f.key] ?? "").trim(),
161
+ )));
162
+ const canTest = !isSubmitting && !isDialogTesting && (isEdit || canSubmit);
163
+
164
+ const handleSubmit = () => {
165
+ if (isEdit) void onUpdate();
166
+ else void onCreate();
167
+ };
168
+
169
+ return (
170
+ <CodemationDialog onClose={onClose} testId="credential-dialog" size="wide">
171
+ <CodemationDialog.Title>{isEdit ? "Edit credential" : "Add credential"}</CodemationDialog.Title>
172
+ <CodemationDialog.Content>
173
+ <CredentialDialogFormSections
174
+ credentialTypes={credentialTypes}
175
+ typesLoading={typesLoading}
176
+ typesError={typesError}
177
+ typesEmpty={typesEmpty}
178
+ selectedTypeId={selectedTypeId}
179
+ setSelectedTypeId={setSelectedTypeId}
180
+ displayName={displayName}
181
+ setDisplayName={setDisplayName}
182
+ sourceKind={sourceKind}
183
+ setSourceKind={setSourceKind}
184
+ isEdit={isEdit}
185
+ isTypeLocked={isTypeLocked}
186
+ canToggleSecrets={canToggleSecrets}
187
+ showSecrets={showSecrets}
188
+ setShowSecrets={setShowSecrets}
189
+ secretsLoading={secretsLoading}
190
+ isOAuth2Type={isOAuth2Type}
191
+ oauth2RedirectUri={oauth2RedirectUri}
192
+ isLoadingOauth2RedirectUri={isLoadingOauth2RedirectUri}
193
+ editingInstance={editingInstance}
194
+ canSubmit={canSubmit}
195
+ onConnectOAuth2={onConnectOAuth2}
196
+ onDisconnectOAuth2={onDisconnectOAuth2}
197
+ />
198
+ <CredentialDialogFieldRows
199
+ orderedFields={orderedFields}
200
+ publicFieldValues={publicFieldValues}
201
+ setPublicFieldValues={setPublicFieldValues}
202
+ secretFieldValues={secretFieldValues}
203
+ setSecretFieldValues={setSecretFieldValues}
204
+ envRefValues={envRefValues}
205
+ setEnvRefValues={setEnvRefValues}
206
+ isEdit={isEdit}
207
+ isDbSecretSource={isDbSecretSource}
208
+ showSecrets={showSecrets}
209
+ credentialFieldEnvStatus={credentialFieldEnvStatus}
210
+ />
211
+ <CredentialDialogFeedback errorMessage={errorMessage} dialogTestResult={dialogTestResult} />
212
+ </CodemationDialog.Content>
213
+ <CodemationDialog.Actions>
214
+ <Button type="button" variant="outline" className="gap-1.5" onClick={onClose}>
215
+ <X className="size-4 shrink-0" aria-hidden />
216
+ Cancel
217
+ </Button>
218
+ <Button
219
+ type="button"
220
+ variant="secondary"
221
+ className="gap-1.5"
222
+ data-testid="credential-test-button"
223
+ disabled={!canTest}
224
+ onClick={() => void onTest()}
225
+ >
226
+ <span className="inline-flex items-center gap-1.5">
227
+ <FlaskConical className="size-4 shrink-0" aria-hidden />
228
+ <span className="leading-none">{isDialogTesting ? "Testing…" : "Test"}</span>
229
+ </span>
230
+ </Button>
231
+ <Button
232
+ type="button"
233
+ className="gap-1.5"
234
+ data-testid={isEdit ? "credential-save-button" : "credential-create-button"}
235
+ disabled={!canSubmit}
236
+ onClick={handleSubmit}
237
+ >
238
+ <span className="inline-flex items-center gap-1.5">
239
+ {isEdit ? (
240
+ <Save className="size-4 shrink-0" aria-hidden />
241
+ ) : (
242
+ <PlusCircle className="size-4 shrink-0" aria-hidden />
243
+ )}
244
+ <span className="leading-none">
245
+ {isSubmitting ? (isEdit ? "Saving…" : "Creating…") : isEdit ? "Save" : "Create"}
246
+ </span>
247
+ </span>
248
+ </Button>
249
+ </CodemationDialog.Actions>
250
+ </CodemationDialog>
251
+ );
252
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { Alert, AlertDescription } from "@/components/ui/alert";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export type CredentialDialogFeedbackProps = {
8
+ errorMessage: string | null;
9
+ dialogTestResult: { status: string; message?: string } | null;
10
+ };
11
+
12
+ export function CredentialDialogFeedback({ errorMessage, dialogTestResult }: CredentialDialogFeedbackProps) {
13
+ return (
14
+ <>
15
+ {errorMessage && (
16
+ <Alert variant="destructive" data-testid="credentials-error">
17
+ <AlertDescription>{errorMessage}</AlertDescription>
18
+ </Alert>
19
+ )}
20
+ {dialogTestResult && (
21
+ <Badge
22
+ variant={dialogTestResult.status === "healthy" ? "secondary" : "destructive"}
23
+ data-testid="credential-dialog-test-result"
24
+ className={cn(
25
+ "h-auto min-h-8 whitespace-normal px-2.5 py-1.5 text-left font-normal",
26
+ dialogTestResult.status === "healthy" &&
27
+ "border-emerald-500/30 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200",
28
+ )}
29
+ >
30
+ {dialogTestResult.status === "healthy" ? "Healthy" : "Failing"}
31
+ {dialogTestResult.message ? `: ${dialogTestResult.message}` : ""}
32
+ </Badge>
33
+ )}
34
+ </>
35
+ );
36
+ }
@@ -0,0 +1,257 @@
1
+ "use client";
2
+
3
+ import type { CredentialFieldSchema } from "@codemation/core/browser";
4
+ import type { Dispatch, SetStateAction } from "react";
5
+
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import { CredentialEnvFieldStatusRow } from "./CredentialEnvFieldStatusRow";
10
+ import { CredentialFieldCopyButton } from "./CredentialFieldCopyButton";
11
+ import { isCredentialFieldLockedByEnv, maskedDisplayValue } from "../lib/credentialFieldHelpers";
12
+
13
+ export type CredentialDialogOrderedField =
14
+ | { kind: "public"; field: CredentialFieldSchema; order: number }
15
+ | { kind: "secret"; field: CredentialFieldSchema; order: number };
16
+
17
+ export type CredentialDialogFieldRowsProps = {
18
+ orderedFields: ReadonlyArray<CredentialDialogOrderedField>;
19
+ publicFieldValues: Record<string, string>;
20
+ setPublicFieldValues: Dispatch<SetStateAction<Record<string, string>>>;
21
+ secretFieldValues: Record<string, string>;
22
+ setSecretFieldValues: Dispatch<SetStateAction<Record<string, string>>>;
23
+ envRefValues: Record<string, string>;
24
+ setEnvRefValues: Dispatch<SetStateAction<Record<string, string>>>;
25
+ isEdit: boolean;
26
+ isDbSecretSource: boolean;
27
+ showSecrets: boolean;
28
+ credentialFieldEnvStatus: Readonly<Record<string, boolean>>;
29
+ };
30
+
31
+ function envVarNameTrimmed(field: CredentialFieldSchema): string | undefined {
32
+ const n = field.envVarName?.trim();
33
+ return n && n.length > 0 ? n : undefined;
34
+ }
35
+
36
+ function isEnvMissingInHost(
37
+ field: CredentialFieldSchema,
38
+ credentialFieldEnvStatus: Readonly<Record<string, boolean>>,
39
+ ): boolean {
40
+ const name = envVarNameTrimmed(field);
41
+ if (!name) {
42
+ return false;
43
+ }
44
+ return credentialFieldEnvStatus[name] === false;
45
+ }
46
+
47
+ export function CredentialDialogFieldRows({
48
+ orderedFields,
49
+ publicFieldValues,
50
+ setPublicFieldValues,
51
+ secretFieldValues,
52
+ setSecretFieldValues,
53
+ envRefValues,
54
+ setEnvRefValues,
55
+ isEdit,
56
+ isDbSecretSource,
57
+ showSecrets,
58
+ credentialFieldEnvStatus,
59
+ }: CredentialDialogFieldRowsProps) {
60
+ return (
61
+ <>
62
+ {orderedFields.map(({ kind, field }) => {
63
+ const lockedByEnv = isCredentialFieldLockedByEnv(field, credentialFieldEnvStatus);
64
+ const envMissing = isEnvMissingInHost(field, credentialFieldEnvStatus);
65
+ /** Red "not set in host" notice is only relevant when editing an existing credential. */
66
+ const showEnvMissingNotice = isEdit && envMissing;
67
+ const showFieldInputs = !lockedByEnv;
68
+ const copyValue = field.copyValue?.trim();
69
+ const showCopy = Boolean(copyValue && showFieldInputs);
70
+
71
+ if (kind === "public") {
72
+ const id = `credential-public-${field.key}`;
73
+ return (
74
+ <div key={`public-${field.key}`} className="flex flex-col gap-2">
75
+ <div className="flex flex-wrap items-center justify-between gap-2">
76
+ <Label htmlFor={showFieldInputs ? id : undefined}>
77
+ {field.label}
78
+ {field.required ? " *" : ""}
79
+ </Label>
80
+ {showCopy && copyValue ? (
81
+ <CredentialFieldCopyButton
82
+ value={copyValue}
83
+ label={field.copyButtonLabel}
84
+ testId={`credential-field-copy-${field.key}`}
85
+ />
86
+ ) : null}
87
+ </div>
88
+ {lockedByEnv && envVarNameTrimmed(field) ? (
89
+ <div data-testid={`credential-public-${field.key}`}>
90
+ <CredentialEnvFieldStatusRow
91
+ kind="managed"
92
+ envVarName={envVarNameTrimmed(field)!}
93
+ fieldKey={field.key}
94
+ />
95
+ </div>
96
+ ) : null}
97
+ {showEnvMissingNotice && envVarNameTrimmed(field) ? (
98
+ <CredentialEnvFieldStatusRow
99
+ kind="missing"
100
+ envVarName={envVarNameTrimmed(field)!}
101
+ fieldKey={field.key}
102
+ />
103
+ ) : null}
104
+ {showFieldInputs ? (
105
+ field.type === "textarea" ? (
106
+ <Textarea
107
+ id={id}
108
+ data-testid={`credential-public-${field.key}`}
109
+ rows={4}
110
+ value={publicFieldValues[field.key] ?? ""}
111
+ onChange={(e) => setPublicFieldValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
112
+ placeholder={field.placeholder}
113
+ />
114
+ ) : (
115
+ <Input
116
+ id={id}
117
+ data-testid={`credential-public-${field.key}`}
118
+ type={field.type === "password" ? "password" : "text"}
119
+ value={publicFieldValues[field.key] ?? ""}
120
+ onChange={(e) => setPublicFieldValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
121
+ placeholder={field.placeholder}
122
+ />
123
+ )
124
+ ) : null}
125
+ {showFieldInputs && field.helpText ? (
126
+ <span className="text-xs text-muted-foreground">{field.helpText}</span>
127
+ ) : null}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ if (isDbSecretSource) {
133
+ const raw = secretFieldValues[field.key] ?? "";
134
+ const isMasked = isEdit && field.type === "password" && !showSecrets && raw.length > 0;
135
+ const displayValue = isMasked ? maskedDisplayValue() : raw;
136
+ const id = `credential-secret-${field.key}`;
137
+ return (
138
+ <div key={`secret-${field.key}`} className="flex flex-col gap-2">
139
+ <div className="flex flex-wrap items-center justify-between gap-2">
140
+ <Label htmlFor={showFieldInputs ? id : undefined}>
141
+ {field.label}
142
+ {field.required ? " *" : ""}
143
+ </Label>
144
+ {showCopy && copyValue ? (
145
+ <CredentialFieldCopyButton
146
+ value={copyValue}
147
+ label={field.copyButtonLabel}
148
+ testId={`credential-field-copy-${field.key}`}
149
+ />
150
+ ) : null}
151
+ </div>
152
+ {lockedByEnv && envVarNameTrimmed(field) ? (
153
+ <div data-testid={`credential-secret-${field.key}`}>
154
+ <CredentialEnvFieldStatusRow
155
+ kind="managed"
156
+ envVarName={envVarNameTrimmed(field)!}
157
+ fieldKey={field.key}
158
+ />
159
+ </div>
160
+ ) : null}
161
+ {showEnvMissingNotice && envVarNameTrimmed(field) ? (
162
+ <CredentialEnvFieldStatusRow
163
+ kind="missing"
164
+ envVarName={envVarNameTrimmed(field)!}
165
+ fieldKey={field.key}
166
+ />
167
+ ) : null}
168
+ {showFieldInputs ? (
169
+ field.type === "textarea" ? (
170
+ <Textarea
171
+ id={id}
172
+ data-testid={`credential-secret-${field.key}`}
173
+ rows={4}
174
+ value={displayValue}
175
+ onChange={(e) => setSecretFieldValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
176
+ readOnly={isMasked}
177
+ placeholder={isEdit ? undefined : field.placeholder}
178
+ />
179
+ ) : (
180
+ <Input
181
+ id={id}
182
+ data-testid={`credential-secret-${field.key}`}
183
+ type={
184
+ showSecrets && field.type === "password"
185
+ ? "text"
186
+ : field.type === "password"
187
+ ? "password"
188
+ : "text"
189
+ }
190
+ value={displayValue}
191
+ onChange={(e) => setSecretFieldValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
192
+ readOnly={isMasked}
193
+ placeholder={isEdit ? undefined : field.placeholder}
194
+ />
195
+ )
196
+ ) : null}
197
+ {showFieldInputs && field.helpText ? (
198
+ <span className="text-xs text-muted-foreground">{field.helpText}</span>
199
+ ) : null}
200
+ {isEdit && showFieldInputs && !lockedByEnv && (
201
+ <span className="text-xs text-muted-foreground">Leave blank to keep existing value</span>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ const displayEnv = envRefValues[field.key] ?? "";
208
+ const id = `credential-env-${field.key}`;
209
+ return (
210
+ <div key={`env-${field.key}`} className="flex flex-col gap-2">
211
+ <div className="flex flex-wrap items-center justify-between gap-2">
212
+ <Label htmlFor={showFieldInputs ? id : undefined}>
213
+ Env var for {field.label}
214
+ {field.required ? " *" : ""}
215
+ </Label>
216
+ {showCopy && copyValue ? (
217
+ <CredentialFieldCopyButton
218
+ value={copyValue}
219
+ label={field.copyButtonLabel}
220
+ testId={`credential-field-copy-${field.key}`}
221
+ />
222
+ ) : null}
223
+ </div>
224
+ {lockedByEnv && envVarNameTrimmed(field) ? (
225
+ <div data-testid={`credential-env-${field.key}`}>
226
+ <CredentialEnvFieldStatusRow
227
+ kind="managed"
228
+ envVarName={envVarNameTrimmed(field)!}
229
+ fieldKey={field.key}
230
+ />
231
+ </div>
232
+ ) : null}
233
+ {showEnvMissingNotice && envVarNameTrimmed(field) ? (
234
+ <CredentialEnvFieldStatusRow kind="missing" envVarName={envVarNameTrimmed(field)!} fieldKey={field.key} />
235
+ ) : null}
236
+ {showFieldInputs ? (
237
+ <Input
238
+ id={id}
239
+ data-testid={`credential-env-${field.key}`}
240
+ type="text"
241
+ value={displayEnv}
242
+ onChange={(e) => setEnvRefValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
243
+ placeholder={isEdit ? undefined : (field.placeholder ?? `e.g. GMAIL_${field.key.toUpperCase()}`)}
244
+ />
245
+ ) : null}
246
+ {showFieldInputs && field.helpText ? (
247
+ <span className="text-xs text-muted-foreground">{field.helpText}</span>
248
+ ) : null}
249
+ {isEdit && showFieldInputs && !lockedByEnv && (
250
+ <span className="text-xs text-muted-foreground">Leave blank to keep existing value</span>
251
+ )}
252
+ </div>
253
+ );
254
+ })}
255
+ </>
256
+ );
257
+ }