@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.
- package/README.md +25 -0
- package/app/(shell)/credentials/page.tsx +5 -0
- package/app/(shell)/dashboard/page.tsx +14 -0
- package/app/(shell)/layout.tsx +11 -0
- package/app/(shell)/page.tsx +5 -0
- package/app/(shell)/users/page.tsx +5 -0
- package/app/(shell)/workflows/[workflowId]/page.tsx +19 -0
- package/app/(shell)/workflows/page.tsx +5 -0
- package/app/api/[[...path]]/route.ts +40 -0
- package/app/api/auth/[...nextauth]/route.ts +3 -0
- package/app/globals.css +997 -0
- package/app/invite/[token]/page.tsx +10 -0
- package/app/layout.tsx +65 -0
- package/app/login/layout.tsx +25 -0
- package/app/login/page.tsx +22 -0
- package/components.json +21 -0
- package/docs/FORMS.md +46 -0
- package/docs/TAILWIND_SHADCN_MIGRATION.md +89 -0
- package/eslint.config.mjs +56 -0
- package/middleware.ts +29 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +34 -0
- package/package.json +76 -0
- package/postcss.config.mjs +7 -0
- package/public/canvas-icons/builtin/openai.svg +5 -0
- package/src/api/CodemationApiClient.ts +107 -0
- package/src/api/CodemationApiHttpError.ts +17 -0
- package/src/auth/CodemationNextAuthConfigResolver.ts +14 -0
- package/src/auth/CodemationNextAuthOAuthProviderDescriptorMapper.ts +30 -0
- package/src/auth/CodemationNextAuthOAuthProviderSnapshotResolver.ts +17 -0
- package/src/auth/CodemationNextAuthProviderCatalog.ts +107 -0
- package/src/auth/codemationEdgeAuth.ts +25 -0
- package/src/auth/codemationNextAuth.ts +32 -0
- package/src/components/Codemation.tsx +6 -0
- package/src/components/CodemationDataTable.tsx +37 -0
- package/src/components/CodemationDialog.tsx +137 -0
- package/src/components/CodemationFormattedDateTime.tsx +46 -0
- package/src/components/GoogleColorGIcon.tsx +39 -0
- package/src/components/OauthProviderIcon.tsx +33 -0
- package/src/components/PasswordStrengthMeter.tsx +59 -0
- package/src/components/forms/index.ts +28 -0
- package/src/components/json/JsonMonacoEditor.tsx +75 -0
- package/src/components/oauthProviderIconData.ts +17 -0
- package/src/components/ui/alert.tsx +56 -0
- package/src/components/ui/badge.tsx +40 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +70 -0
- package/src/components/ui/collapsible.tsx +26 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/dropdown-menu.tsx +238 -0
- package/src/components/ui/form.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +169 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/switch.tsx +28 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +76 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle.tsx +41 -0
- package/src/features/credentials/components/CredentialConfirmDialog.tsx +58 -0
- package/src/features/credentials/components/CredentialDialog.tsx +252 -0
- package/src/features/credentials/components/CredentialDialogFeedback.tsx +36 -0
- package/src/features/credentials/components/CredentialDialogFieldRows.tsx +257 -0
- package/src/features/credentials/components/CredentialDialogFormSections.tsx +230 -0
- package/src/features/credentials/components/CredentialEnvFieldStatusRow.tsx +64 -0
- package/src/features/credentials/components/CredentialFieldCopyButton.tsx +48 -0
- package/src/features/credentials/components/CredentialsScreenHealthBadge.tsx +21 -0
- package/src/features/credentials/components/CredentialsScreenInstancesTable.tsx +108 -0
- package/src/features/credentials/components/CredentialsScreenTestFailureAlert.tsx +33 -0
- package/src/features/credentials/hooks/useCredentialCreateDialog.ts +33 -0
- package/src/features/credentials/hooks/useCredentialDialogSession.ts +616 -0
- package/src/features/credentials/hooks/useCredentialsScreen.ts +213 -0
- package/src/features/credentials/lib/credentialFieldHelpers.ts +35 -0
- package/src/features/credentials/lib/credentialFormTypes.ts +1 -0
- package/src/features/credentials/lib/credentialInstanceTestPayloadParser.ts +10 -0
- package/src/features/credentials/screens/CredentialsScreen.tsx +187 -0
- package/src/features/invite/screens/InviteAcceptScreen.tsx +190 -0
- package/src/features/users/components/UsersInviteDialog.tsx +121 -0
- package/src/features/users/components/UsersRegenerateDialog.tsx +81 -0
- package/src/features/users/components/UsersScreenUserStatusBadge.tsx +19 -0
- package/src/features/users/schemas/usersInviteFormSchema.ts +7 -0
- package/src/features/users/screens/UsersScreen.tsx +240 -0
- package/src/features/workflows/components/WorkflowListFolderSection.tsx +91 -0
- package/src/features/workflows/components/WorkflowListItemCard.tsx +67 -0
- package/src/features/workflows/components/WorkflowListRoot.tsx +39 -0
- package/src/features/workflows/components/WorkflowsListTree.tsx +28 -0
- package/src/features/workflows/components/canvas/CanvasNodeChromeTooltip.tsx +96 -0
- package/src/features/workflows/components/canvas/CanvasNodeIconSlot.tsx +25 -0
- package/src/features/workflows/components/canvas/VisibleNodeStatusResolver.tsx +84 -0
- package/src/features/workflows/components/canvas/WorkflowCanvas.tsx +248 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNode.tsx +182 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAccents.tsx +73 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentBottomSourceHandles.tsx +43 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentLabels.tsx +47 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeCard.tsx +202 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeHandles.tsx +77 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeLabelBelow.tsx +51 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeMainGlyph.tsx +64 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeToolbar.tsx +95 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasLoadingPlaceholder.tsx +69 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasNodeIcon.tsx +102 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasSimpleIconGlyph.tsx +21 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasStraightCountEdge.tsx +33 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasStructureSignature.tsx +7 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasSymmetricForkEdge.tsx +32 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasToolbarIconButton.tsx +95 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasBuiltinIconRegistry.ts +26 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeCountResolver.ts +51 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeStyleResolver.ts +35 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasLabelLayoutEstimator.ts +42 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasOverlapResolver.ts +78 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasPortOrderResolver.ts +25 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts +56 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasSiIconRegistry.ts +18 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasSymmetricForkPathPlanner.ts +43 -0
- package/src/features/workflows/components/canvas/lib/layoutWorkflow.ts +315 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasEdgeGeometry.ts +3 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasEmbeddedStyles.ts +62 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasFlowTypes.ts +10 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasNodeData.ts +41 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasNodeGeometry.ts +99 -0
- package/src/features/workflows/components/canvas/workflowCanvasNodeChrome.tsx +46 -0
- package/src/features/workflows/components/realtime/RealtimeContext.tsx +14 -0
- package/src/features/workflows/components/realtime/WorkflowRealtimeProvider.tsx +15 -0
- package/src/features/workflows/components/workflowDetail/NodeCredentialBindingRow.tsx +209 -0
- package/src/features/workflows/components/workflowDetail/NodeCredentialBindingsSection.tsx +227 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesConfigSection.tsx +51 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesPanelHeader.tsx +50 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesSlidePanel.tsx +134 -0
- package/src/features/workflows/components/workflowDetail/WorkflowActivationErrorDialog.tsx +71 -0
- package/src/features/workflows/components/workflowDetail/WorkflowActivationHeaderControl.tsx +64 -0
- package/src/features/workflows/components/workflowDetail/WorkflowDetailIcons.tsx +52 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspector.tsx +110 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorDetailBody.tsx +213 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorPanes.tsx +239 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorSidebarResizer.tsx +31 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorTreePanel.tsx +133 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentGroupingPresenter.tsx +31 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentList.tsx +118 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorBinaryView.tsx +15 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorErrorView.tsx +107 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorJsonView.tsx +114 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreePresenter.tsx +132 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreeViewRenderer.tsx +147 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyView.tsx +65 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorViews.tsx +5 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryAttachmentRow.tsx +74 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryUploadRow.tsx +69 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorDialog.tsx +254 -0
- package/src/features/workflows/components/workflowDetail/WorkflowRunsList.tsx +89 -0
- package/src/features/workflows/components/workflowDetail/WorkflowRunsSidebar.tsx +50 -0
- package/src/features/workflows/hooks/canvas/useWorkflowCanvasVisibleNodeStatuses.ts +14 -0
- package/src/features/workflows/hooks/realtime/realtime.tsx +271 -0
- package/src/features/workflows/hooks/realtime/runQueryPolling.ts +34 -0
- package/src/features/workflows/hooks/realtime/useWorkflowRealtimeInfrastructure.ts +541 -0
- package/src/features/workflows/hooks/realtime/useWorkflowRealtimeShowDisconnectedBadge.ts +9 -0
- package/src/features/workflows/hooks/workflowDetail/useWorkflowDetailController.tsx +1300 -0
- package/src/features/workflows/lib/realtime/realtimeApi.ts +78 -0
- package/src/features/workflows/lib/realtime/realtimeClientBridge.ts +52 -0
- package/src/features/workflows/lib/realtime/realtimeDomainTypes.ts +191 -0
- package/src/features/workflows/lib/realtime/realtimeQueryKeys.ts +15 -0
- package/src/features/workflows/lib/realtime/realtimeRunMutations.ts +167 -0
- package/src/features/workflows/lib/realtime/workflowTypes.ts +5 -0
- package/src/features/workflows/lib/workflowDetail/PersistedWorkflowSnapshotMapper.ts +205 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowActivationHttpErrorFormat.ts +32 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowDetailPresenter.ts +1017 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowDetailUrlCodec.ts +70 -0
- package/src/features/workflows/lib/workflowDetail/workflowDetailTypes.ts +152 -0
- package/src/features/workflows/lib/workflowDetailTreeStyles.ts +65 -0
- package/src/features/workflows/screens/WorkflowDetailScreen.tsx +236 -0
- package/src/features/workflows/screens/WorkflowDetailScreenInspectorPanel.tsx +55 -0
- package/src/features/workflows/screens/WorkflowsList.tsx +35 -0
- package/src/features/workflows/screens/WorkflowsScreen.tsx +31 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/src/middleware/CodemationNextHostMiddlewarePathRules.ts +31 -0
- package/src/providers/CodemationSessionProvider.tsx +23 -0
- package/src/providers/Providers.tsx +36 -0
- package/src/providers/RealtimeBoundary.tsx +17 -0
- package/src/providers/WhitelabelProvider.tsx +22 -0
- package/src/server/CodemationAuthPrismaClient.ts +21 -0
- package/src/server/CodemationNextHost.ts +379 -0
- package/src/shell/AppLayout.tsx +141 -0
- package/src/shell/AppLayoutNavItems.tsx +129 -0
- package/src/shell/AppLayoutPageHeader.tsx +79 -0
- package/src/shell/AppLayoutSidebarBrand.tsx +33 -0
- package/src/shell/AppMainContent.tsx +17 -0
- package/src/shell/AppShellHeaderActions.tsx +12 -0
- package/src/shell/AppShellHeaderActionsAuthenticated.tsx +51 -0
- package/src/shell/CodemationNextClientShell.tsx +17 -0
- package/src/shell/CredentialsSignInRedirectResolver.ts +21 -0
- package/src/shell/LoginPageClient.tsx +231 -0
- package/src/shell/WorkflowDetailChromeContext.tsx +42 -0
- package/src/shell/WorkflowFolderTreeBuilder.ts +62 -0
- package/src/shell/WorkflowFolderUi.ts +42 -0
- package/src/shell/WorkflowSidebarNavFolder.tsx +112 -0
- package/src/shell/WorkflowSidebarNavTree.tsx +68 -0
- package/src/shell/appLayoutPageTitle.ts +16 -0
- package/src/shell/appLayoutSidebarIcons.tsx +108 -0
- package/src/whitelabel/CodemationWhitelabelSnapshot.ts +4 -0
- package/src/whitelabel/CodemationWhitelabelSnapshotFactory.ts +18 -0
- package/tsconfig.json +40 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NextAuthConfig } from "next-auth";
|
|
2
|
+
|
|
3
|
+
export type CodemationOAuthProviderDescriptor = Readonly<{
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export class CodemationNextAuthOAuthProviderDescriptorMapper {
|
|
9
|
+
mapFromBuiltProviders(
|
|
10
|
+
providers: ReadonlyArray<NonNullable<NextAuthConfig["providers"]>[number]>,
|
|
11
|
+
): ReadonlyArray<CodemationOAuthProviderDescriptor> {
|
|
12
|
+
const out: CodemationOAuthProviderDescriptor[] = [];
|
|
13
|
+
for (const p of providers) {
|
|
14
|
+
if (p === null || p === undefined) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (typeof p !== "object") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const id = "id" in p && typeof (p as { id: unknown }).id === "string" ? (p as { id: string }).id : "";
|
|
21
|
+
if (id === "" || id === "credentials") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const rawName = "name" in p ? (p as { name: unknown }).name : undefined;
|
|
25
|
+
const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : id;
|
|
26
|
+
out.push({ id, name });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CodemationAuthPrismaClient } from "../server/CodemationAuthPrismaClient";
|
|
2
|
+
import type { CodemationOAuthProviderDescriptor } from "./CodemationNextAuthOAuthProviderDescriptorMapper";
|
|
3
|
+
import { CodemationNextAuthOAuthProviderDescriptorMapper } from "./CodemationNextAuthOAuthProviderDescriptorMapper";
|
|
4
|
+
import { CodemationNextAuthConfigResolver } from "./CodemationNextAuthConfigResolver";
|
|
5
|
+
import { CodemationNextAuthProviderCatalog } from "./CodemationNextAuthProviderCatalog";
|
|
6
|
+
|
|
7
|
+
export type { CodemationOAuthProviderDescriptor } from "./CodemationNextAuthOAuthProviderDescriptorMapper";
|
|
8
|
+
|
|
9
|
+
export class CodemationNextAuthOAuthProviderSnapshotResolver {
|
|
10
|
+
async resolve(): Promise<ReadonlyArray<CodemationOAuthProviderDescriptor>> {
|
|
11
|
+
const env = process.env;
|
|
12
|
+
const authConfig = await new CodemationNextAuthConfigResolver().resolve();
|
|
13
|
+
const prisma = await CodemationAuthPrismaClient.resolveShared();
|
|
14
|
+
const built = await CodemationNextAuthProviderCatalog.build(authConfig, prisma, env);
|
|
15
|
+
return new CodemationNextAuthOAuthProviderDescriptorMapper().mapFromBuiltProviders(built);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodemationAuthConfig,
|
|
3
|
+
CodemationAuthOAuthProviderConfig,
|
|
4
|
+
CodemationAuthOidcProviderConfig,
|
|
5
|
+
} from "@codemation/host";
|
|
6
|
+
import type { PrismaClient } from "@codemation/host/persistence";
|
|
7
|
+
import { compare } from "bcryptjs";
|
|
8
|
+
import type { NextAuthConfig } from "next-auth";
|
|
9
|
+
import Credentials from "next-auth/providers/credentials";
|
|
10
|
+
import GitHub from "next-auth/providers/github";
|
|
11
|
+
import Google from "next-auth/providers/google";
|
|
12
|
+
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
|
|
13
|
+
|
|
14
|
+
export class CodemationNextAuthProviderCatalog {
|
|
15
|
+
static async build(
|
|
16
|
+
authConfig: CodemationAuthConfig | undefined,
|
|
17
|
+
prisma: PrismaClient,
|
|
18
|
+
env: NodeJS.ProcessEnv,
|
|
19
|
+
): Promise<NextAuthConfig["providers"]> {
|
|
20
|
+
if (!authConfig) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const providers: NextAuthConfig["providers"] = [];
|
|
24
|
+
if (CodemationNextAuthProviderCatalog.includesCredentialsProvider(authConfig)) {
|
|
25
|
+
providers.push(CodemationNextAuthProviderCatalog.createCredentialsProvider(prisma));
|
|
26
|
+
}
|
|
27
|
+
for (const entry of authConfig.oauth ?? []) {
|
|
28
|
+
providers.push(CodemationNextAuthProviderCatalog.createOAuthProvider(entry, env));
|
|
29
|
+
}
|
|
30
|
+
for (const entry of authConfig.oidc ?? []) {
|
|
31
|
+
providers.push(CodemationNextAuthProviderCatalog.createOidcProvider(entry, env));
|
|
32
|
+
}
|
|
33
|
+
return providers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static includesCredentialsProvider(authConfig: CodemationAuthConfig): boolean {
|
|
37
|
+
return authConfig.kind === "local";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static createCredentialsProvider(prisma: PrismaClient): NextAuthConfig["providers"][number] {
|
|
41
|
+
return Credentials({
|
|
42
|
+
name: "Email and password",
|
|
43
|
+
credentials: {
|
|
44
|
+
email: { label: "Email", type: "email" },
|
|
45
|
+
password: { label: "Password", type: "password" },
|
|
46
|
+
},
|
|
47
|
+
authorize: async (credentials) => {
|
|
48
|
+
const email = typeof credentials?.email === "string" ? credentials.email.trim() : "";
|
|
49
|
+
const password = typeof credentials?.password === "string" ? credentials.password : "";
|
|
50
|
+
if (!email || !password) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
54
|
+
if (!user?.passwordHash || user.accountStatus === "inactive") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (user.accountStatus !== "active") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const matches = await compare(password, user.passwordHash);
|
|
61
|
+
if (!matches) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
id: user.id,
|
|
66
|
+
email: user.email ?? email,
|
|
67
|
+
name: user.name ?? undefined,
|
|
68
|
+
image: user.image ?? undefined,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static createOAuthProvider(
|
|
75
|
+
entry: CodemationAuthOAuthProviderConfig,
|
|
76
|
+
env: NodeJS.ProcessEnv,
|
|
77
|
+
): NextAuthConfig["providers"][number] {
|
|
78
|
+
const clientId = env[entry.clientIdEnv] ?? "";
|
|
79
|
+
const clientSecret = env[entry.clientSecretEnv] ?? "";
|
|
80
|
+
if (entry.provider === "google") {
|
|
81
|
+
return Google({ clientId, clientSecret });
|
|
82
|
+
}
|
|
83
|
+
if (entry.provider === "github") {
|
|
84
|
+
return GitHub({ clientId, clientSecret });
|
|
85
|
+
}
|
|
86
|
+
const tenantId = entry.tenantIdEnv ? (env[entry.tenantIdEnv] ?? "common") : "common";
|
|
87
|
+
return MicrosoftEntraID({
|
|
88
|
+
clientId,
|
|
89
|
+
clientSecret,
|
|
90
|
+
issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static createOidcProvider(
|
|
95
|
+
entry: CodemationAuthOidcProviderConfig,
|
|
96
|
+
env: NodeJS.ProcessEnv,
|
|
97
|
+
): NextAuthConfig["providers"][number] {
|
|
98
|
+
return {
|
|
99
|
+
id: entry.id,
|
|
100
|
+
name: entry.id,
|
|
101
|
+
type: "oidc",
|
|
102
|
+
issuer: entry.issuer,
|
|
103
|
+
clientId: env[entry.clientIdEnv] ?? "",
|
|
104
|
+
clientSecret: env[entry.clientSecretEnv] ?? "",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import NextAuth from "next-auth";
|
|
2
|
+
import Credentials from "next-auth/providers/credentials";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware runs on the Edge runtime: no Prisma, no consumer manifest.
|
|
6
|
+
* Verifies Auth.js JWT session cookies using AUTH_SECRET only.
|
|
7
|
+
*/
|
|
8
|
+
const authSecretFromEnv = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;
|
|
9
|
+
const authSecret =
|
|
10
|
+
authSecretFromEnv?.trim() ||
|
|
11
|
+
(process.env.NODE_ENV === "development" ? "codemation-dev-auth-secret-not-for-production" : undefined);
|
|
12
|
+
|
|
13
|
+
export const { auth } = NextAuth({
|
|
14
|
+
trustHost: true,
|
|
15
|
+
session: { strategy: "jwt" },
|
|
16
|
+
secret: authSecret,
|
|
17
|
+
providers: [
|
|
18
|
+
Credentials({
|
|
19
|
+
id: "edge-jwt-verifier-placeholder",
|
|
20
|
+
name: "Edge verifier",
|
|
21
|
+
credentials: {},
|
|
22
|
+
authorize: async () => null,
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
2
|
+
import NextAuth from "next-auth";
|
|
3
|
+
import { CodemationAuthPrismaClient } from "../server/CodemationAuthPrismaClient";
|
|
4
|
+
import { CodemationNextAuthConfigResolver } from "./CodemationNextAuthConfigResolver";
|
|
5
|
+
import { CodemationNextAuthProviderCatalog } from "./CodemationNextAuthProviderCatalog";
|
|
6
|
+
|
|
7
|
+
export const { handlers, auth, signIn, signOut } = NextAuth(async () => {
|
|
8
|
+
const env = process.env;
|
|
9
|
+
const authConfig = await new CodemationNextAuthConfigResolver().resolve();
|
|
10
|
+
const prisma = await CodemationAuthPrismaClient.resolveShared();
|
|
11
|
+
const secretFromEnv = env.AUTH_SECRET ?? env.NEXTAUTH_SECRET;
|
|
12
|
+
const secret =
|
|
13
|
+
secretFromEnv?.trim() ||
|
|
14
|
+
(env.NODE_ENV === "development" ? "codemation-dev-auth-secret-not-for-production" : undefined);
|
|
15
|
+
if (!secret || secret.trim().length === 0) {
|
|
16
|
+
throw new Error("AUTH_SECRET (or NEXTAUTH_SECRET) is required for Codemation authentication.");
|
|
17
|
+
}
|
|
18
|
+
const providers = await CodemationNextAuthProviderCatalog.build(authConfig, prisma, env);
|
|
19
|
+
if (env.NODE_ENV === "production" && providers.length === 0) {
|
|
20
|
+
throw new Error("CodemationConfig.auth must configure at least one NextAuth provider for production.");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
adapter: PrismaAdapter(prisma),
|
|
24
|
+
secret,
|
|
25
|
+
session: { strategy: "jwt" },
|
|
26
|
+
providers: [...providers],
|
|
27
|
+
pages: {
|
|
28
|
+
signIn: "/login",
|
|
29
|
+
},
|
|
30
|
+
trustHost: true,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WorkflowSummary } from "../features/workflows/hooks/realtime/realtime";
|
|
2
|
+
import { WorkflowsScreen } from "../features/workflows/screens/WorkflowsScreen";
|
|
3
|
+
|
|
4
|
+
export function Codemation(args: Readonly<{ initialWorkflows: ReadonlyArray<WorkflowSummary> }>) {
|
|
5
|
+
return <WorkflowsScreen {...args} />;
|
|
6
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
6
|
+
|
|
7
|
+
export type CodemationDataTableColumn = Readonly<{
|
|
8
|
+
key: string;
|
|
9
|
+
header: string;
|
|
10
|
+
headerTestId?: string;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export type CodemationDataTableProps = Readonly<{
|
|
14
|
+
tableTestId: string;
|
|
15
|
+
columns: ReadonlyArray<CodemationDataTableColumn>;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared data table using shadcn/ui Table primitives and design tokens.
|
|
21
|
+
*/
|
|
22
|
+
export function CodemationDataTable(props: CodemationDataTableProps) {
|
|
23
|
+
return (
|
|
24
|
+
<Table data-testid={props.tableTestId}>
|
|
25
|
+
<TableHeader>
|
|
26
|
+
<TableRow>
|
|
27
|
+
{props.columns.map((column) => (
|
|
28
|
+
<TableHead key={column.key} data-testid={column.headerTestId ?? `codemation-table-header-${column.key}`}>
|
|
29
|
+
{column.header}
|
|
30
|
+
</TableHead>
|
|
31
|
+
))}
|
|
32
|
+
</TableRow>
|
|
33
|
+
</TableHeader>
|
|
34
|
+
<TableBody>{props.children}</TableBody>
|
|
35
|
+
</Table>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const maxWidthBySize = {
|
|
9
|
+
narrow: "sm:max-w-lg",
|
|
10
|
+
wide: "sm:max-w-2xl",
|
|
11
|
+
full: "sm:max-w-[min(92vw,960px)]",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type CodemationDialogSize = keyof typeof maxWidthBySize;
|
|
15
|
+
|
|
16
|
+
export type CodemationDialogRootProps = Readonly<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
/** Root `data-testid` (applied to the dialog panel). */
|
|
20
|
+
testId?: string;
|
|
21
|
+
/** `dialog` (default) or `alertdialog` for confirmations. */
|
|
22
|
+
role?: "dialog" | "alertdialog";
|
|
23
|
+
/** Max width preset; default `wide`. */
|
|
24
|
+
size?: CodemationDialogSize;
|
|
25
|
+
/** Extra classes on the Radix panel (e.g. `max-h-[min(90vh,640px)]`). */
|
|
26
|
+
contentClassName?: string;
|
|
27
|
+
/** Corner X to dismiss (Radix); default false — use `<CodemationDialog.Actions>` for explicit buttons. */
|
|
28
|
+
showCloseButton?: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
function CodemationDialogRoot({
|
|
32
|
+
children,
|
|
33
|
+
onClose,
|
|
34
|
+
testId,
|
|
35
|
+
role = "dialog",
|
|
36
|
+
size = "wide",
|
|
37
|
+
contentClassName,
|
|
38
|
+
showCloseButton = false,
|
|
39
|
+
}: CodemationDialogRootProps) {
|
|
40
|
+
return (
|
|
41
|
+
<Dialog
|
|
42
|
+
open
|
|
43
|
+
onOpenChange={(open) => {
|
|
44
|
+
if (!open) onClose();
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<DialogContent
|
|
48
|
+
showCloseButton={showCloseButton}
|
|
49
|
+
data-testid={testId}
|
|
50
|
+
role={role}
|
|
51
|
+
aria-describedby={undefined}
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex max-h-[min(92vh,900px)] flex-col gap-0 overflow-hidden p-0",
|
|
54
|
+
maxWidthBySize[size],
|
|
55
|
+
contentClassName,
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</DialogContent>
|
|
60
|
+
</Dialog>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type CodemationDialogTitleProps = Readonly<{
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
className?: string;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Do not set `id` on the underlying Radix `DialogTitle` — the dialog root assigns `titleId`
|
|
71
|
+
* in context; overriding `id` breaks `aria-labelledby` and Radix dev warnings.
|
|
72
|
+
*/
|
|
73
|
+
function CodemationDialogTitle({ children, className }: CodemationDialogTitleProps) {
|
|
74
|
+
return (
|
|
75
|
+
<DialogTitle
|
|
76
|
+
className={cn("m-0 shrink-0 border-b border-border px-4 py-3 text-base leading-none font-semibold", className)}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</DialogTitle>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CodemationDialogContentProps = Readonly<{
|
|
84
|
+
children: React.ReactNode;
|
|
85
|
+
className?: string;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
function CodemationDialogContent({ children, className }: CodemationDialogContentProps) {
|
|
89
|
+
return (
|
|
90
|
+
<div className={cn("flex min-h-0 flex-1 flex-col gap-4 overflow-auto px-4 py-3 text-sm", className)}>
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type CodemationDialogActionsProps = Readonly<{
|
|
97
|
+
children: React.ReactNode;
|
|
98
|
+
/** Toolbar directly under the title (e.g. filters). Default is footer actions. */
|
|
99
|
+
position?: "top" | "bottom";
|
|
100
|
+
/** Flex alignment for the button row. */
|
|
101
|
+
align?: "start" | "end" | "between";
|
|
102
|
+
className?: string;
|
|
103
|
+
}>;
|
|
104
|
+
|
|
105
|
+
function CodemationDialogActions({
|
|
106
|
+
children,
|
|
107
|
+
position = "bottom",
|
|
108
|
+
align = "end",
|
|
109
|
+
className,
|
|
110
|
+
}: CodemationDialogActionsProps) {
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"flex shrink-0 flex-wrap gap-2 border-border bg-muted/30 px-4 py-3",
|
|
115
|
+
position === "top" ? "border-b" : "border-t",
|
|
116
|
+
align === "end" && "justify-end",
|
|
117
|
+
align === "start" && "justify-start",
|
|
118
|
+
align === "between" && "justify-between",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{children}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type CodemationDialogCompound = typeof CodemationDialogRoot & {
|
|
128
|
+
Title: typeof CodemationDialogTitle;
|
|
129
|
+
Content: typeof CodemationDialogContent;
|
|
130
|
+
Actions: typeof CodemationDialogActions;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const CodemationDialog = Object.assign(CodemationDialogRoot, {
|
|
134
|
+
Title: CodemationDialogTitle,
|
|
135
|
+
Content: CodemationDialogContent,
|
|
136
|
+
Actions: CodemationDialogActions,
|
|
137
|
+
}) as CodemationDialogCompound;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Locale } from "date-fns";
|
|
4
|
+
import { format, isValid, parseISO } from "date-fns";
|
|
5
|
+
import { enUS } from "date-fns/locale";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
|
|
8
|
+
export type CodemationFormattedDateTimeProps = Readonly<{
|
|
9
|
+
/** ISO 8601 instant (e.g. from API). */
|
|
10
|
+
isoUtc: string | null | undefined;
|
|
11
|
+
/** Shown when `isoUtc` is missing or unparsable. */
|
|
12
|
+
fallbackText?: string;
|
|
13
|
+
/** Reserved for localization; defaults to `enUS`. */
|
|
14
|
+
locale?: Locale;
|
|
15
|
+
dataTestId?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders a human-readable date/time via **date-fns** (`format` + `parseISO`).
|
|
21
|
+
* Pass a different `locale` when you wire i18n.
|
|
22
|
+
*/
|
|
23
|
+
export function CodemationFormattedDateTime(props: CodemationFormattedDateTimeProps): ReactNode {
|
|
24
|
+
const { isoUtc, fallbackText = "—", locale = enUS, dataTestId, className } = props;
|
|
25
|
+
if (!isoUtc?.trim()) {
|
|
26
|
+
return (
|
|
27
|
+
<span className={className} data-testid={dataTestId}>
|
|
28
|
+
{fallbackText}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const parsed = parseISO(isoUtc);
|
|
33
|
+
if (!isValid(parsed)) {
|
|
34
|
+
return (
|
|
35
|
+
<span className={className} data-testid={dataTestId}>
|
|
36
|
+
{fallbackText}
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const label = format(parsed, "PPp", { locale });
|
|
41
|
+
return (
|
|
42
|
+
<time className={className} dateTime={isoUtc} data-testid={dataTestId}>
|
|
43
|
+
{label}
|
|
44
|
+
</time>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Standard color Google "G" (multicolor). Simple Icons only ships a monochrome mark;
|
|
7
|
+
* Google requires the full-color logo for sign-in UI.
|
|
8
|
+
*
|
|
9
|
+
* @see https://developers.google.com/identity/branding-guidelines
|
|
10
|
+
*/
|
|
11
|
+
export function GoogleColorGIcon(props: Readonly<{ className?: string; testId?: string }>): ReactNode {
|
|
12
|
+
return (
|
|
13
|
+
<svg
|
|
14
|
+
role="img"
|
|
15
|
+
className={props.className}
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
aria-hidden
|
|
19
|
+
data-testid={props.testId}
|
|
20
|
+
>
|
|
21
|
+
<path
|
|
22
|
+
fill="#4285F4"
|
|
23
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
24
|
+
/>
|
|
25
|
+
<path
|
|
26
|
+
fill="#34A853"
|
|
27
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
28
|
+
/>
|
|
29
|
+
<path
|
|
30
|
+
fill="#FBBC05"
|
|
31
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
32
|
+
/>
|
|
33
|
+
<path
|
|
34
|
+
fill="#EA4335"
|
|
35
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Component, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { GoogleColorGIcon } from "./GoogleColorGIcon";
|
|
6
|
+
import { simpleIconForProvider } from "./oauthProviderIconData";
|
|
7
|
+
|
|
8
|
+
export type OauthProviderIconProps = Readonly<{
|
|
9
|
+
providerId: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
testId?: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export class OauthProviderIcon extends Component<OauthProviderIconProps> {
|
|
15
|
+
override render(): ReactNode {
|
|
16
|
+
if (this.props.providerId === "google") {
|
|
17
|
+
return <GoogleColorGIcon className={this.props.className} testId={this.props.testId} />;
|
|
18
|
+
}
|
|
19
|
+
const icon = simpleIconForProvider(this.props.providerId);
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
role="img"
|
|
23
|
+
className={this.props.className}
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
+
aria-hidden
|
|
27
|
+
data-testid={this.props.testId}
|
|
28
|
+
>
|
|
29
|
+
<path d={icon.path} fill={`#${icon.hex}`} />
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, type ReactNode } from "react";
|
|
4
|
+
import zxcvbn from "zxcvbn";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
export type PasswordStrengthMeterProps = Readonly<{
|
|
9
|
+
password: string;
|
|
10
|
+
/** Skips scoring (and layout) when false. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
dataTestId?: string;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
const scoreLabels = ["Too weak", "Weak", "Fair", "Good", "Strong"] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Password strength using **zxcvbn** (Dropbox). Not a policy gate — server still enforces min length.
|
|
19
|
+
*/
|
|
20
|
+
export function PasswordStrengthMeter(props: PasswordStrengthMeterProps): ReactNode {
|
|
21
|
+
const { password, enabled = true, dataTestId = "password-strength-meter" } = props;
|
|
22
|
+
const result = useMemo(() => {
|
|
23
|
+
if (!enabled || password.length === 0) return null;
|
|
24
|
+
return zxcvbn(password);
|
|
25
|
+
}, [enabled, password]);
|
|
26
|
+
|
|
27
|
+
if (!enabled || password.length === 0 || !result) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { score, feedback } = result;
|
|
32
|
+
const hint = feedback.warning || (feedback.suggestions[0] ?? "");
|
|
33
|
+
const label = scoreLabels[Math.min(score, 4)] ?? scoreLabels[0];
|
|
34
|
+
|
|
35
|
+
const barClass = (i: number) =>
|
|
36
|
+
cn(
|
|
37
|
+
"h-1.5 flex-1 rounded-sm bg-muted transition-colors",
|
|
38
|
+
i <= score &&
|
|
39
|
+
(score <= 1 ? "bg-destructive" : score <= 3 ? "bg-amber-500" : "bg-emerald-600 dark:bg-emerald-500"),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col gap-1.5" data-testid={dataTestId} role="status" aria-live="polite">
|
|
44
|
+
<div className="flex gap-1" aria-hidden>
|
|
45
|
+
{([0, 1, 2, 3, 4] as const).map((i) => (
|
|
46
|
+
<span key={i} className={barClass(i)} />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
<span className="text-xs font-medium text-foreground" data-testid={`${dataTestId}-label`}>
|
|
50
|
+
{label}
|
|
51
|
+
</span>
|
|
52
|
+
{hint ? (
|
|
53
|
+
<span className="text-xs text-muted-foreground" data-testid={`${dataTestId}-hint`}>
|
|
54
|
+
{hint}
|
|
55
|
+
</span>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical forms stack: React Hook Form + Zod + shadcn Form primitives.
|
|
3
|
+
* Import from here in feature code so validation and layout stay consistent.
|
|
4
|
+
*/
|
|
5
|
+
export { z } from "zod";
|
|
6
|
+
export { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
export {
|
|
8
|
+
useForm,
|
|
9
|
+
useFormContext,
|
|
10
|
+
useFormState,
|
|
11
|
+
useWatch,
|
|
12
|
+
type FieldPath,
|
|
13
|
+
type FieldValues,
|
|
14
|
+
type Resolver,
|
|
15
|
+
type SubmitHandler,
|
|
16
|
+
type UseFormProps,
|
|
17
|
+
type UseFormReturn,
|
|
18
|
+
} from "react-hook-form";
|
|
19
|
+
export {
|
|
20
|
+
Form,
|
|
21
|
+
FormControl,
|
|
22
|
+
FormDescription,
|
|
23
|
+
FormField,
|
|
24
|
+
FormItem,
|
|
25
|
+
FormLabel,
|
|
26
|
+
FormMessage,
|
|
27
|
+
useFormField,
|
|
28
|
+
} from "@/components/ui/form";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Editor from "@monaco-editor/react";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
|
|
6
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
7
|
+
|
|
8
|
+
const defaultOptions: NonNullable<ComponentProps<typeof Editor>["options"]> = {
|
|
9
|
+
automaticLayout: true,
|
|
10
|
+
formatOnPaste: true,
|
|
11
|
+
formatOnType: true,
|
|
12
|
+
minimap: { enabled: false },
|
|
13
|
+
scrollBeyondLastLine: false,
|
|
14
|
+
lineNumbersMinChars: 3,
|
|
15
|
+
tabSize: 2,
|
|
16
|
+
insertSpaces: true,
|
|
17
|
+
wordWrap: "on",
|
|
18
|
+
bracketPairColorization: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
},
|
|
21
|
+
guides: {
|
|
22
|
+
indentation: true,
|
|
23
|
+
bracketPairs: true,
|
|
24
|
+
},
|
|
25
|
+
padding: {
|
|
26
|
+
top: 12,
|
|
27
|
+
bottom: 12,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Monaco-based JSON editor with a mirrored, visually hidden `<textarea>` that carries the same value.
|
|
33
|
+
* Tests and automation can drive `data-testid` on that textarea because Monaco’s surface is not a reliable
|
|
34
|
+
* DOM target for `fireEvent.change` / user typing simulation.
|
|
35
|
+
*/
|
|
36
|
+
export function JsonMonacoEditor(
|
|
37
|
+
args: Readonly<{
|
|
38
|
+
path: string;
|
|
39
|
+
value: string;
|
|
40
|
+
onChange: (value: string | undefined) => void;
|
|
41
|
+
/** Shown below the editor region when set. */
|
|
42
|
+
error?: string | null;
|
|
43
|
+
/** Passed to the hidden textarea for stable test selectors. */
|
|
44
|
+
testId?: string;
|
|
45
|
+
}>,
|
|
46
|
+
) {
|
|
47
|
+
const { path, value, onChange, error, testId = "workflow-json-editor-input" } = args;
|
|
48
|
+
return (
|
|
49
|
+
<div className="relative flex min-h-0 flex-1 flex-col">
|
|
50
|
+
<div className="h-[min(60vh,560px)] min-h-[200px] shrink-0 overflow-hidden rounded-md border border-border bg-background">
|
|
51
|
+
<Editor
|
|
52
|
+
height="100%"
|
|
53
|
+
language="json"
|
|
54
|
+
path={path}
|
|
55
|
+
value={value}
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
loading={<div className="grid h-full place-items-center text-xs text-muted-foreground">Loading editor…</div>}
|
|
58
|
+
options={defaultOptions}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<Textarea
|
|
62
|
+
data-testid={testId}
|
|
63
|
+
value={value}
|
|
64
|
+
onChange={(event) => {
|
|
65
|
+
onChange(event.target.value);
|
|
66
|
+
}}
|
|
67
|
+
spellCheck={false}
|
|
68
|
+
className="pointer-events-none absolute inset-0 h-px w-px min-h-0 resize-none border-0 p-0 opacity-0"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
/>
|
|
72
|
+
{error ? <div className="mt-1 text-xs text-destructive">{error}</div> : null}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|