From 5c1689fa2c5daf642ff65087744cb879c86776b8 Mon Sep 17 00:00:00 2001 From: Prajwal Aradhya Date: Fri, 14 Aug 2026 16:41:27 +0100 Subject: [PATCH] feat(portal): route canvas header nav + create/settings route forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canvas header now carries navigation instead of dev-era metadata: a back link to the project's route list and a pagination-style RouteSwitcher that steps through sibling routes or jumps to one via a dropdown. Dropped the read-only toggle and the blocks/edges/unsaved counters — the Save button already says whether anything is pending. MethodSwitch moves off hardcoded hex onto the theme tokens so it renders in both light and dark. Co-Authored-By: Claude Opus 5 --- .../src/components/canvas/CanvasWorkbench.tsx | 44 +- .../integrations/IntegrationForm.tsx | 11 +- .../IntegrationOnboardingForm.tsx | 227 +++++++-- .../IntegrationOnboardingModal.tsx | 6 +- .../integrations/connectors/AiForm.tsx | 22 +- .../connectors/CredentialsUrlForm.tsx | 27 +- .../connectors/ObservabilityForm.tsx | 43 +- .../components/routes/RouteSettingsModal.tsx | 458 +++++++++++++++++ .../src/components/routes/RouteSwitcher.tsx | 139 ++++++ .../src/components/routes/routeForm.tsx | 185 +++++++ apps/portal/src/query/routesQuery.ts | 14 + .../_authed/$projectId/integrations.tsx | 27 +- .../src/routes/_authed/$projectId/routes.tsx | 140 +----- .../routes/_authed/$projectId/routes_.new.tsx | 470 ++++++++++++++++++ .../_authed/$projectId_.canvas.$routeId.tsx | 42 +- apps/portal/src/services/routes.ts | 8 + apps/server/src/api/v1/routes/create/dto.ts | 1 + apps/server/src/api/v1/routes/update/dto.ts | 4 + .../src/api/v1/routes/update/service.ts | 12 +- .../src/SchemaEditor/ConfigurationDrawer.tsx | 40 +- .../src/SchemaEditor/DataTypeSelect.tsx | 11 +- .../src/SchemaEditor/PropertyRow.tsx | 49 +- .../src/SchemaEditor/SchemaNavigator.tsx | 13 +- .../components/src/SchemaEditor/context.tsx | 5 + packages/components/src/SchemaEditor/index.ts | 1 + packages/components/src/SchemaEditor/types.ts | 7 + .../components/src/SchemaEditor/utils.test.ts | 14 + packages/components/src/SchemaEditor/utils.ts | 19 + 28 files changed, 1718 insertions(+), 321 deletions(-) create mode 100644 apps/portal/src/components/routes/RouteSettingsModal.tsx create mode 100644 apps/portal/src/components/routes/RouteSwitcher.tsx create mode 100644 apps/portal/src/components/routes/routeForm.tsx create mode 100644 apps/portal/src/routes/_authed/$projectId/routes_.new.tsx diff --git a/apps/portal/src/components/canvas/CanvasWorkbench.tsx b/apps/portal/src/components/canvas/CanvasWorkbench.tsx index f21949ed..09f09acf 100644 --- a/apps/portal/src/components/canvas/CanvasWorkbench.tsx +++ b/apps/portal/src/components/canvas/CanvasWorkbench.tsx @@ -22,6 +22,10 @@ export type CanvasWorkbenchProps = { /** Opt-in only: the route owning the playground controls enables its trigger. */ enablePlayground?: boolean; playgroundContent?: ReactNode; + /** Extra header controls owned by the caller — route settings, for one. */ + headerActions?: ReactNode; + /** Replaces the plain title on the left — the route switcher, for one. */ + headerLeft?: ReactNode; /** re-read from the server, for diagnosing a rejected save */ reload: () => Promise; save: (payload: CanvasSavePayload) => Promise; @@ -50,8 +54,9 @@ export function CanvasWorkbench({ enableBlockPicker = false, enablePlayground = false, playgroundContent, + headerActions, + headerLeft, }: CanvasWorkbenchProps) { - const [readOnly, setReadOnly] = useState(false); const [pendingCount, setPendingCount] = useState(0); const [isSaving, setIsSaving] = useState(false); const [cycleFeedbackToken, setCycleFeedbackToken] = useState(0); @@ -94,30 +99,17 @@ export function CanvasWorkbench({ return (
-
- {title} - - - {graph.blocks.length} blocks · {graph.edges.length} edges ·{" "} - {pendingCount} unsaved - - {!readOnly && ( - - )} +
+ {headerLeft ?? {title}} +
{headerActions}
+
@@ -132,7 +124,7 @@ export function CanvasWorkbench({ ) : ( )} + diff --git a/apps/portal/src/components/integrations/IntegrationOnboardingForm.tsx b/apps/portal/src/components/integrations/IntegrationOnboardingForm.tsx index 6ccd2253..5c6716b4 100644 --- a/apps/portal/src/components/integrations/IntegrationOnboardingForm.tsx +++ b/apps/portal/src/components/integrations/IntegrationOnboardingForm.tsx @@ -6,6 +6,7 @@ import { } from "react-icons/fa6"; import { TbArrowLeft, + TbBolt, TbCheck, TbCloudCog, TbDatabase, @@ -35,11 +36,11 @@ const CRED_PLACEHOLDERS: Record; ssl?: bool }; const GROUP_DETAILS: Record = { - database: { description: "Connect a production database", icon: }, - kv: { description: "Connect a cache or key-value store", icon: }, - ai: { description: "Connect an AI provider or compatible endpoint", icon: }, - baas: { description: "Connect a managed backend service", icon: }, - observability: { description: "Send logs and telemetry to your stack", icon: }, + database: { description: "Connect a production database", icon: }, + kv: { description: "Connect a cache or key-value store", icon: }, + ai: { description: "Connect an AI provider or compatible endpoint", icon: }, + baas: { description: "Connect a managed backend service", icon: }, + observability: { description: "Send logs and telemetry to your stack", icon: }, }; function setPath(obj: Record, path: string, value: unknown) { @@ -57,6 +58,7 @@ function setPath(obj: Record, path: string, value: unknown) { export function IntegrationOnboardingForm({ projectId, onSaved }: { projectId: string; onSaved?: () => void }) { const create = integrationsQuery.create.mutation(projectId); + const test = integrationsQuery.testConnection.mutation(projectId); const [step, setStep] = useState(1); const [group, setGroup] = useState(""); const [variant, setVariant] = useState(""); @@ -92,6 +94,28 @@ export function IntegrationOnboardingForm({ projectId, onSaved }: { projectId: s setStep(nextStep); } + function testIntegration() { + const schema = getSchema(group as never, variant as never); + if (!schema) { + toast.danger("Invalid connector selection"); + return; + } + const parsed = schema.safeParse(config); + if (!parsed.success) { + toast.danger(parsed.error.issues[0]?.message ?? "Invalid configuration"); + return; + } + + test.mutate( + { group, variant, config: parsed.data }, + { + onSuccess: (res) => + res?.success ? toast.success("Connection successful") : toast.danger(res?.error ?? "Connection failed"), + onError: (error) => showErrorNotification(error as Error), + }, + ); + } + function saveIntegration() { const schema = getSchema(group as never, variant as never); if (!schema) { @@ -120,8 +144,8 @@ export function IntegrationOnboardingForm({ projectId, onSaved }: { projectId: s const groupName = group ? humanReadableConnectorNames[group as keyof typeof humanReadableConnectorNames] : ""; return ( -
-