@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,616 @@
1
+ import { ApiPaths } from "@codemation/host-src/presentation/http/ApiPaths";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { codemationApiClient } from "../../../api/CodemationApiClient";
5
+ import { CodemationApiHttpError } from "../../../api/CodemationApiHttpError";
6
+ import {
7
+ useCredentialFieldEnvStatusQuery,
8
+ useCredentialInstancesQuery,
9
+ useCredentialInstanceWithSecretsQuery,
10
+ useCredentialTypesQuery,
11
+ type CredentialInstanceDto,
12
+ } from "../../workflows/hooks/realtime/realtime";
13
+ import { credentialFieldEnvStatusQueryKey } from "../../workflows/lib/realtime/realtimeQueryKeys";
14
+ import { parseCredentialInstanceTestPayload } from "../lib/credentialInstanceTestPayloadParser";
15
+ import { buildEmptySecretFieldValues, buildFieldStringValues } from "../lib/credentialFieldHelpers";
16
+ import type { FormSourceKind } from "../lib/credentialFormTypes";
17
+ import type { CredentialDialogProps } from "../components/CredentialDialog";
18
+
19
+ type DialogMode = "create" | "edit" | null;
20
+
21
+ export type CredentialDialogSessionOptions = Readonly<{
22
+ workflowId?: string;
23
+ onCredentialCreated?: (instance: CredentialInstanceDto) => void;
24
+ closeAfterCreatePolicy: "always" | "unless_oauth2";
25
+ oauthConnectedPolicy: "close_dialog" | "refresh_only";
26
+ /** When true, build {@link CredentialDialogProps} for embedding; the credentials page passes props manually. */
27
+ buildDialogProps: boolean;
28
+ }>;
29
+
30
+ /**
31
+ * Shared create/edit/test/OAuth state for {@link useCredentialsScreen} and {@link useCredentialCreateDialog}.
32
+ */
33
+ export function useCredentialDialogSession(options: CredentialDialogSessionOptions) {
34
+ const { workflowId, onCredentialCreated, closeAfterCreatePolicy, oauthConnectedPolicy, buildDialogProps } = options;
35
+ const queryClient = useQueryClient();
36
+ const [dialogMode, setDialogMode] = useState<DialogMode>(null);
37
+ const [editingInstanceId, setEditingInstanceId] = useState<string | null>(null);
38
+ const [acceptedTypeFilter, setAcceptedTypeFilter] = useState<ReadonlyArray<string> | null>(null);
39
+ const credentialTypesQuery = useCredentialTypesQuery();
40
+ const credentialFieldEnvStatusQuery = useCredentialFieldEnvStatusQuery();
41
+ const credentialInstancesQuery = useCredentialInstancesQuery();
42
+ const credentialWithSecretsQuery = useCredentialInstanceWithSecretsQuery(
43
+ dialogMode === "edit" ? editingInstanceId : null,
44
+ );
45
+ const credentialTypesAll = credentialTypesQuery.data ?? [];
46
+ const credentialFieldEnvStatus = credentialFieldEnvStatusQuery.data ?? {};
47
+ const credentialInstances = credentialInstancesQuery.data ?? [];
48
+ const credentialTypes = useMemo(() => {
49
+ if (!acceptedTypeFilter || acceptedTypeFilter.length === 0) {
50
+ return credentialTypesAll;
51
+ }
52
+ return credentialTypesAll.filter((t) => acceptedTypeFilter.includes(t.typeId));
53
+ }, [acceptedTypeFilter, credentialTypesAll]);
54
+
55
+ const [selectedTypeId, setSelectedTypeId] = useState<string>("");
56
+ const [displayName, setDisplayName] = useState("");
57
+ const [sourceKind, setSourceKind] = useState<FormSourceKind>("db");
58
+ const [publicFieldValues, setPublicFieldValues] = useState<Record<string, string>>({});
59
+ const [secretFieldValues, setSecretFieldValues] = useState<Record<string, string>>({});
60
+ const [envRefValues, setEnvRefValues] = useState<Record<string, string>>({});
61
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
62
+ const [isSubmitting, setIsSubmitting] = useState(false);
63
+ const [editDisplayName, setEditDisplayName] = useState("");
64
+ const [editPublicFieldValues, setEditPublicFieldValues] = useState<Record<string, string>>({});
65
+ const [editSecretFieldValues, setEditSecretFieldValues] = useState<Record<string, string>>({});
66
+ const [editEnvRefValues, setEditEnvRefValues] = useState<Record<string, string>>({});
67
+ const [isEditSubmitting, setIsEditSubmitting] = useState(false);
68
+ const [showSecrets, setShowSecrets] = useState(false);
69
+ const [oauth2RedirectUri, setOauth2RedirectUri] = useState<string>("");
70
+ const [isLoadingOauth2RedirectUri, setIsLoadingOauth2RedirectUri] = useState(false);
71
+ const [dialogTestResult, setDialogTestResult] = useState<{ status: string; message?: string } | null>(null);
72
+ const [isDialogTesting, setIsDialogTesting] = useState(false);
73
+ const [oauthDisconnectConfirmOpen, setOauthDisconnectConfirmOpen] = useState(false);
74
+
75
+ const selectedType = useMemo(
76
+ () => credentialTypes.find((type) => type.typeId === selectedTypeId),
77
+ [credentialTypes, selectedTypeId],
78
+ );
79
+ const secretFields = selectedType?.secretFields ?? [];
80
+ const editingInstance = useMemo(
81
+ () => (editingInstanceId ? credentialInstances.find((i) => i.instanceId === editingInstanceId) : null),
82
+ [credentialInstances, editingInstanceId],
83
+ );
84
+ const editingType = useMemo(
85
+ () => credentialTypesAll.find((t) => t.typeId === editingInstance?.typeId),
86
+ [credentialTypesAll, editingInstance?.typeId],
87
+ );
88
+
89
+ const resetCreateForm = useCallback(() => {
90
+ setPublicFieldValues(selectedType ? buildFieldStringValues(selectedType.publicFields ?? []) : {});
91
+ setSecretFieldValues(selectedType ? buildEmptySecretFieldValues(selectedType.secretFields ?? []) : {});
92
+ setEnvRefValues(selectedType ? buildEmptySecretFieldValues(selectedType.secretFields ?? []) : {});
93
+ }, [selectedType]);
94
+
95
+ useEffect(() => {
96
+ resetCreateForm();
97
+ }, [selectedTypeId, sourceKind, resetCreateForm]);
98
+
99
+ const refreshQueries = useCallback(async (): Promise<void> => {
100
+ const tasks = [
101
+ queryClient.invalidateQueries({ queryKey: ["credential-instances"] }),
102
+ queryClient.invalidateQueries({ queryKey: ["credential-types"] }),
103
+ queryClient.invalidateQueries({ queryKey: ["credential-instance-with-secrets"] }),
104
+ queryClient.invalidateQueries({ queryKey: credentialFieldEnvStatusQueryKey }),
105
+ ];
106
+ if (workflowId) {
107
+ tasks.push(queryClient.invalidateQueries({ queryKey: ["workflow-credential-health", workflowId] }));
108
+ }
109
+ await Promise.all(tasks);
110
+ }, [queryClient, workflowId]);
111
+
112
+ const closeDialog = useCallback(() => {
113
+ setDialogMode(null);
114
+ setEditingInstanceId(null);
115
+ setAcceptedTypeFilter(null);
116
+ setSelectedTypeId("");
117
+ setDisplayName("");
118
+ setSourceKind("db");
119
+ setPublicFieldValues({});
120
+ setSecretFieldValues({});
121
+ setEnvRefValues({});
122
+ setErrorMessage(null);
123
+ setDialogTestResult(null);
124
+ setShowSecrets(false);
125
+ setOauth2RedirectUri("");
126
+ setOauthDisconnectConfirmOpen(false);
127
+ }, []);
128
+
129
+ const openCreateDialog = useCallback(
130
+ (acceptedTypeIds?: ReadonlyArray<string>) => {
131
+ setDialogMode("create");
132
+ setEditingInstanceId(null);
133
+ setAcceptedTypeFilter(acceptedTypeIds && acceptedTypeIds.length > 0 ? [...acceptedTypeIds] : null);
134
+ setErrorMessage(null);
135
+ setDialogTestResult(null);
136
+ setShowSecrets(false);
137
+ setSourceKind("db");
138
+ const types =
139
+ acceptedTypeIds && acceptedTypeIds.length > 0
140
+ ? credentialTypesAll.filter((t) => acceptedTypeIds.includes(t.typeId))
141
+ : credentialTypesAll;
142
+ const first = types[0];
143
+ setSelectedTypeId(first?.typeId ?? "");
144
+ setDisplayName("");
145
+ setPublicFieldValues(first ? buildFieldStringValues(first.publicFields ?? []) : {});
146
+ setSecretFieldValues(first ? buildEmptySecretFieldValues(first.secretFields ?? []) : {});
147
+ setEnvRefValues(first ? buildEmptySecretFieldValues(first.secretFields ?? []) : {});
148
+ },
149
+ [credentialTypesAll],
150
+ );
151
+
152
+ const openEditDialog = useCallback(
153
+ (instance: CredentialInstanceDto): void => {
154
+ const instanceType = credentialTypes.find((type) => type.typeId === instance.typeId);
155
+ setDialogMode("edit");
156
+ setEditingInstanceId(instance.instanceId);
157
+ setSelectedTypeId(instance.typeId);
158
+ setEditDisplayName(instance.displayName);
159
+ setEditPublicFieldValues(buildFieldStringValues(instanceType?.publicFields ?? [], instance.publicConfig));
160
+ setEditSecretFieldValues({});
161
+ setEditEnvRefValues({});
162
+ setShowSecrets(false);
163
+ setErrorMessage(null);
164
+ setDialogTestResult(null);
165
+ },
166
+ [credentialTypes],
167
+ );
168
+
169
+ const ensureDialogCredentialInstance = useCallback(async (): Promise<CredentialInstanceDto | null> => {
170
+ if (dialogMode === "edit") {
171
+ return editingInstance ?? null;
172
+ }
173
+ if (!selectedType) {
174
+ return null;
175
+ }
176
+ try {
177
+ setIsSubmitting(true);
178
+ setErrorMessage(null);
179
+ setDialogTestResult(null);
180
+ const secretConfig =
181
+ sourceKind === "db"
182
+ ? (Object.fromEntries(secretFields.map((f) => [f.key, secretFieldValues[f.key] ?? ""])) as Record<
183
+ string,
184
+ unknown
185
+ >)
186
+ : undefined;
187
+ const envSecretRefs =
188
+ sourceKind === "env"
189
+ ? (Object.fromEntries(
190
+ secretFields
191
+ .map((f) => [f.key, (envRefValues[f.key] ?? "").trim()] as const)
192
+ .filter(([, v]) => v.length > 0),
193
+ ) as Record<string, string>)
194
+ : undefined;
195
+ const publicConfig = Object.fromEntries(
196
+ (selectedType.publicFields ?? []).map((field) => [field.key, (publicFieldValues[field.key] ?? "").trim()]),
197
+ ) as Record<string, unknown>;
198
+ const created = await codemationApiClient.postJson<CredentialInstanceDto>(ApiPaths.credentialInstances(), {
199
+ typeId: selectedTypeId,
200
+ displayName: displayName.trim(),
201
+ sourceKind,
202
+ publicConfig,
203
+ secretConfig,
204
+ envSecretRefs,
205
+ });
206
+ const createdType = credentialTypesAll.find((type) => type.typeId === created.typeId);
207
+ setDialogMode("edit");
208
+ setEditingInstanceId(created.instanceId);
209
+ setSelectedTypeId(created.typeId);
210
+ setEditDisplayName(created.displayName);
211
+ setEditPublicFieldValues(buildFieldStringValues(createdType?.publicFields ?? [], created.publicConfig));
212
+ if (created.sourceKind === "db") {
213
+ setEditSecretFieldValues({ ...secretFieldValues });
214
+ setEditEnvRefValues(buildEmptySecretFieldValues(createdType?.secretFields ?? []));
215
+ } else {
216
+ setEditSecretFieldValues(buildEmptySecretFieldValues(createdType?.secretFields ?? []));
217
+ setEditEnvRefValues({ ...envRefValues });
218
+ }
219
+ await refreshQueries();
220
+ onCredentialCreated?.(created);
221
+ return created;
222
+ } catch (error) {
223
+ setErrorMessage(error instanceof Error ? error.message : String(error));
224
+ return null;
225
+ } finally {
226
+ setIsSubmitting(false);
227
+ }
228
+ }, [
229
+ credentialTypesAll,
230
+ dialogMode,
231
+ editingInstance,
232
+ envRefValues,
233
+ onCredentialCreated,
234
+ publicFieldValues,
235
+ refreshQueries,
236
+ secretFieldValues,
237
+ secretFields,
238
+ selectedType,
239
+ selectedTypeId,
240
+ sourceKind,
241
+ ]);
242
+
243
+ const createCredentialInstance = useCallback(async (): Promise<void> => {
244
+ const created = await ensureDialogCredentialInstance();
245
+ if (!created) {
246
+ return;
247
+ }
248
+ if (closeAfterCreatePolicy === "always") {
249
+ closeDialog();
250
+ return;
251
+ }
252
+ const t = credentialTypesAll.find((type) => type.typeId === created.typeId);
253
+ if (t?.auth?.kind !== "oauth2") {
254
+ closeDialog();
255
+ }
256
+ }, [closeAfterCreatePolicy, closeDialog, credentialTypesAll, ensureDialogCredentialInstance]);
257
+
258
+ const updateCredentialInstance = useCallback(async (): Promise<void> => {
259
+ if (!editingInstanceId || !editingType || !editingInstance) return;
260
+ const fetchedWithSecrets = credentialWithSecretsQuery.data;
261
+ try {
262
+ setIsEditSubmitting(true);
263
+ setErrorMessage(null);
264
+ const secretFieldsEdit = editingType.secretFields ?? [];
265
+ const isDb = editingInstance.sourceKind === "db";
266
+ const secretConfig = isDb
267
+ ? (Object.fromEntries(
268
+ secretFieldsEdit.map((f) => {
269
+ const edited = (editSecretFieldValues[f.key] ?? "").trim();
270
+ const existing = fetchedWithSecrets?.secretConfig?.[f.key] ?? "";
271
+ return [f.key, edited.length > 0 ? edited : existing];
272
+ }),
273
+ ) as Record<string, unknown>)
274
+ : undefined;
275
+ const envSecretRefs =
276
+ !isDb && editingInstance.sourceKind === "env"
277
+ ? (Object.fromEntries(
278
+ secretFieldsEdit
279
+ .map((f) => {
280
+ const edited = (editEnvRefValues[f.key] ?? "").trim();
281
+ const existing = fetchedWithSecrets?.envSecretRefs?.[f.key] ?? "";
282
+ return [f.key, edited.length > 0 ? edited : existing] as const;
283
+ })
284
+ .filter(([, v]) => v.length > 0),
285
+ ) as Record<string, string>)
286
+ : undefined;
287
+ const hasSecretUpdates =
288
+ (isDb && secretConfig && Object.values(secretConfig).some((v) => String(v).length > 0)) ||
289
+ (envSecretRefs && Object.keys(envSecretRefs).length > 0);
290
+ const updateBody: {
291
+ displayName: string;
292
+ publicConfig: Record<string, unknown>;
293
+ secretConfig?: Record<string, unknown>;
294
+ envSecretRefs?: Record<string, string>;
295
+ } = {
296
+ publicConfig: Object.fromEntries(
297
+ (editingType.publicFields ?? []).map((field) => [field.key, (editPublicFieldValues[field.key] ?? "").trim()]),
298
+ ),
299
+ displayName: editDisplayName.trim(),
300
+ };
301
+ if (hasSecretUpdates) {
302
+ if (isDb && secretConfig) updateBody.secretConfig = secretConfig;
303
+ if (envSecretRefs) updateBody.envSecretRefs = envSecretRefs;
304
+ }
305
+ await codemationApiClient.putJson(ApiPaths.credentialInstance(editingInstanceId), updateBody);
306
+ closeDialog();
307
+ await refreshQueries();
308
+ } catch (error) {
309
+ setErrorMessage(error instanceof Error ? error.message : String(error));
310
+ } finally {
311
+ setIsEditSubmitting(false);
312
+ }
313
+ }, [
314
+ closeDialog,
315
+ editEnvRefValues,
316
+ editPublicFieldValues,
317
+ editSecretFieldValues,
318
+ editingInstance,
319
+ editingInstanceId,
320
+ editingType,
321
+ credentialWithSecretsQuery.data,
322
+ refreshQueries,
323
+ ]);
324
+
325
+ const connectOAuth2Credential = useCallback(async (): Promise<void> => {
326
+ if (typeof window === "undefined") {
327
+ return;
328
+ }
329
+ const targetInstance = await ensureDialogCredentialInstance();
330
+ if (!targetInstance) {
331
+ return;
332
+ }
333
+ setErrorMessage(null);
334
+ const popup = window.open(
335
+ ApiPaths.oauth2Auth(targetInstance.instanceId),
336
+ `codemation-oauth2-${targetInstance.instanceId}`,
337
+ "popup=yes,width=640,height=760",
338
+ );
339
+ if (!popup) {
340
+ setErrorMessage("The OAuth popup was blocked by the browser.");
341
+ }
342
+ }, [ensureDialogCredentialInstance]);
343
+
344
+ const executeOAuthDisconnect = useCallback(async (): Promise<void> => {
345
+ if (!editingInstanceId) {
346
+ setOauthDisconnectConfirmOpen(false);
347
+ return;
348
+ }
349
+ const instanceId = editingInstanceId;
350
+ try {
351
+ setOauthDisconnectConfirmOpen(false);
352
+ setErrorMessage(null);
353
+ await codemationApiClient.postJson(ApiPaths.oauth2Disconnect(instanceId));
354
+ await refreshQueries();
355
+ } catch (error) {
356
+ setErrorMessage(error instanceof Error ? error.message : String(error));
357
+ }
358
+ }, [editingInstanceId, refreshQueries]);
359
+
360
+ useEffect(() => {
361
+ const activeType = dialogMode === "edit" ? editingType : selectedType;
362
+ if (activeType?.auth?.kind !== "oauth2") {
363
+ setOauth2RedirectUri("");
364
+ return;
365
+ }
366
+ let cancelled = false;
367
+ const loadRedirectUri = async (): Promise<void> => {
368
+ try {
369
+ setIsLoadingOauth2RedirectUri(true);
370
+ const data = await codemationApiClient.getJson<{ redirectUri?: string }>(ApiPaths.oauth2RedirectUri());
371
+ if (!cancelled) {
372
+ setOauth2RedirectUri(data.redirectUri ?? "");
373
+ }
374
+ } catch (error) {
375
+ if (!cancelled) {
376
+ setErrorMessage(error instanceof Error ? error.message : String(error));
377
+ }
378
+ } finally {
379
+ if (!cancelled) {
380
+ setIsLoadingOauth2RedirectUri(false);
381
+ }
382
+ }
383
+ };
384
+ void loadRedirectUri();
385
+ return () => {
386
+ cancelled = true;
387
+ };
388
+ }, [dialogMode, editingType, selectedType]);
389
+
390
+ useEffect(() => {
391
+ const handleMessage = (event: MessageEvent): void => {
392
+ if (typeof window === "undefined" || event.origin !== window.location.origin) {
393
+ return;
394
+ }
395
+ const data = event.data as Readonly<{
396
+ kind?: string;
397
+ instanceId?: string;
398
+ connectedEmail?: string;
399
+ message?: string;
400
+ }>;
401
+ if (data.kind === "oauth2.connected") {
402
+ void refreshQueries();
403
+ setErrorMessage(null);
404
+ if (oauthConnectedPolicy === "close_dialog") {
405
+ closeDialog();
406
+ }
407
+ return;
408
+ }
409
+ if (data.kind === "oauth2.error") {
410
+ setErrorMessage(data.message ?? "OAuth2 connection failed.");
411
+ }
412
+ };
413
+ window.addEventListener("message", handleMessage);
414
+ return () => {
415
+ window.removeEventListener("message", handleMessage);
416
+ };
417
+ }, [closeDialog, oauthConnectedPolicy, refreshQueries]);
418
+
419
+ useEffect(() => {
420
+ const data = credentialWithSecretsQuery.data;
421
+ if (!data || editingInstanceId !== data.instanceId || dialogMode !== "edit") return;
422
+ const type = credentialTypesAll.find((t) => t.typeId === data.typeId);
423
+ const fields = type?.secretFields ?? [];
424
+ setEditPublicFieldValues(buildFieldStringValues(type?.publicFields ?? [], data.publicConfig));
425
+ if (data.sourceKind === "db" && data.secretConfig) {
426
+ const values = Object.fromEntries(fields.map((f) => [f.key, data.secretConfig![f.key] ?? ""]));
427
+ setEditSecretFieldValues(values);
428
+ setEditEnvRefValues(Object.fromEntries(fields.map((f) => [f.key, ""])));
429
+ } else if (data.sourceKind === "env" && data.envSecretRefs) {
430
+ setEditSecretFieldValues(Object.fromEntries(fields.map((f) => [f.key, ""])));
431
+ const envValues = Object.fromEntries(fields.map((f) => [f.key, data.envSecretRefs![f.key] ?? ""]));
432
+ setEditEnvRefValues(envValues);
433
+ }
434
+ }, [credentialWithSecretsQuery.data, credentialTypesAll, dialogMode, editingInstanceId]);
435
+
436
+ const testCredentialFromDialog = useCallback(async (): Promise<void> => {
437
+ const targetInstance = await ensureDialogCredentialInstance();
438
+ if (!targetInstance) {
439
+ return;
440
+ }
441
+ try {
442
+ setIsDialogTesting(true);
443
+ setDialogTestResult(null);
444
+ setErrorMessage(null);
445
+ const data = await codemationApiClient.postJson<{ status?: string; message?: string }>(
446
+ ApiPaths.credentialInstanceTest(targetInstance.instanceId),
447
+ );
448
+ setDialogTestResult({
449
+ status: data?.status ?? "healthy",
450
+ message: data?.message,
451
+ });
452
+ await refreshQueries();
453
+ } catch (error) {
454
+ if (error instanceof CodemationApiHttpError) {
455
+ const parsed = parseCredentialInstanceTestPayload(error.bodyText);
456
+ setDialogTestResult({
457
+ status: "failing",
458
+ message: parsed.message ?? "Test failed",
459
+ });
460
+ return;
461
+ }
462
+ setDialogTestResult({
463
+ status: "failing",
464
+ message: error instanceof Error ? error.message : String(error),
465
+ });
466
+ } finally {
467
+ setIsDialogTesting(false);
468
+ }
469
+ }, [ensureDialogCredentialInstance, refreshQueries]);
470
+
471
+ const typesLoading = credentialTypesQuery.isLoading;
472
+ const typesError = credentialTypesQuery.isError;
473
+ const typesEmpty = credentialTypes.length === 0;
474
+
475
+ const dialogProps: CredentialDialogProps | null = useMemo(() => {
476
+ if (!buildDialogProps || dialogMode === null) {
477
+ return null;
478
+ }
479
+ const isEdit = dialogMode === "edit";
480
+ return {
481
+ mode: isEdit ? "edit" : "create",
482
+ credentialTypes: isEdit ? credentialTypesAll : credentialTypes,
483
+ typesLoading,
484
+ typesError,
485
+ typesEmpty,
486
+ selectedTypeId,
487
+ setSelectedTypeId,
488
+ displayName: isEdit ? editDisplayName : displayName,
489
+ setDisplayName: isEdit ? setEditDisplayName : setDisplayName,
490
+ sourceKind,
491
+ setSourceKind,
492
+ publicFieldValues: isEdit ? editPublicFieldValues : publicFieldValues,
493
+ setPublicFieldValues: isEdit ? setEditPublicFieldValues : setPublicFieldValues,
494
+ secretFieldValues: isEdit ? editSecretFieldValues : secretFieldValues,
495
+ setSecretFieldValues: isEdit ? setEditSecretFieldValues : setSecretFieldValues,
496
+ envRefValues: isEdit ? editEnvRefValues : envRefValues,
497
+ setEnvRefValues: isEdit ? setEditEnvRefValues : setEnvRefValues,
498
+ showSecrets,
499
+ setShowSecrets,
500
+ oauth2RedirectUri,
501
+ isLoadingOauth2RedirectUri,
502
+ secretsLoading: credentialWithSecretsQuery.isLoading,
503
+ editingInstance: isEdit ? editingInstance : undefined,
504
+ errorMessage,
505
+ dialogTestResult,
506
+ isSubmitting: isEdit ? isEditSubmitting : isSubmitting,
507
+ isDialogTesting,
508
+ onCreate: createCredentialInstance,
509
+ onUpdate: updateCredentialInstance,
510
+ onTest: testCredentialFromDialog,
511
+ onConnectOAuth2: connectOAuth2Credential,
512
+ onDisconnectOAuth2: () => setOauthDisconnectConfirmOpen(true),
513
+ onClose: closeDialog,
514
+ credentialFieldEnvStatus,
515
+ };
516
+ }, [
517
+ buildDialogProps,
518
+ connectOAuth2Credential,
519
+ createCredentialInstance,
520
+ credentialFieldEnvStatus,
521
+ credentialTypes,
522
+ credentialTypesAll,
523
+ credentialWithSecretsQuery.isLoading,
524
+ dialogMode,
525
+ dialogTestResult,
526
+ displayName,
527
+ editDisplayName,
528
+ editEnvRefValues,
529
+ editPublicFieldValues,
530
+ editSecretFieldValues,
531
+ editingInstance,
532
+ envRefValues,
533
+ errorMessage,
534
+ isDialogTesting,
535
+ isEditSubmitting,
536
+ isSubmitting,
537
+ oauth2RedirectUri,
538
+ isLoadingOauth2RedirectUri,
539
+ publicFieldValues,
540
+ secretFieldValues,
541
+ selectedTypeId,
542
+ showSecrets,
543
+ sourceKind,
544
+ testCredentialFromDialog,
545
+ typesEmpty,
546
+ typesError,
547
+ typesLoading,
548
+ updateCredentialInstance,
549
+ closeDialog,
550
+ ]);
551
+
552
+ const cancelOAuthDisconnect = useCallback(() => {
553
+ setOauthDisconnectConfirmOpen(false);
554
+ }, []);
555
+
556
+ return {
557
+ credentialInstances,
558
+ credentialTypes,
559
+ credentialTypesAll,
560
+ credentialFieldEnvStatus,
561
+ credentialTypesQuery,
562
+ credentialInstancesQuery,
563
+ credentialWithSecretsQuery,
564
+ editingInstance,
565
+ editingType,
566
+ dialogMode,
567
+ editingInstanceId,
568
+ selectedTypeId,
569
+ setSelectedTypeId,
570
+ displayName,
571
+ setDisplayName,
572
+ editDisplayName,
573
+ setEditDisplayName,
574
+ sourceKind,
575
+ setSourceKind,
576
+ publicFieldValues,
577
+ setPublicFieldValues,
578
+ secretFieldValues,
579
+ setSecretFieldValues,
580
+ envRefValues,
581
+ setEnvRefValues,
582
+ editPublicFieldValues,
583
+ setEditPublicFieldValues,
584
+ editSecretFieldValues,
585
+ setEditSecretFieldValues,
586
+ editEnvRefValues,
587
+ setEditEnvRefValues,
588
+ showSecrets,
589
+ setShowSecrets,
590
+ oauth2RedirectUri,
591
+ isLoadingOauth2RedirectUri,
592
+ errorMessage,
593
+ setErrorMessage,
594
+ dialogTestResult,
595
+ isSubmitting,
596
+ isEditSubmitting,
597
+ isDialogTesting,
598
+ typesLoading,
599
+ typesError,
600
+ typesEmpty,
601
+ createCredentialInstance,
602
+ updateCredentialInstance,
603
+ testCredentialFromDialog,
604
+ connectOAuth2Credential,
605
+ closeDialog,
606
+ openCreateDialog,
607
+ openEditDialog,
608
+ refreshQueries,
609
+ ensureDialogCredentialInstance,
610
+ dialogProps,
611
+ oauthDisconnectConfirmOpen,
612
+ setOauthDisconnectConfirmOpen,
613
+ executeOAuthDisconnect,
614
+ cancelOAuthDisconnect,
615
+ };
616
+ }