From fdb21b3f48e5211bf56527319e01d62719f728cd Mon Sep 17 00:00:00 2001 From: indraraj Date: Mon, 25 May 2026 16:51:36 +0530 Subject: [PATCH 1/5] debezium/dbz#1978 Update the connection edit view page UX Signed-off-by: indraraj --- debezium-platform-stage/CLAUDE.md | 255 ++++++++++++++++++ .../src/assets/ibm-db2.png | Bin 2828 -> 0 bytes .../src/assets/ibm_db2.svg | 1 + .../src/components/ComponentImage.tsx | 2 +- .../SourceDestinationSelectionList.tsx | 17 +- .../src/pages/Connection/CreateConnection.tsx | 18 +- .../src/pages/Connection/EditConnection.css | 14 + .../src/pages/Connection/EditConnection.tsx | 38 ++- 8 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 debezium-platform-stage/CLAUDE.md delete mode 100644 debezium-platform-stage/src/assets/ibm-db2.png create mode 100644 debezium-platform-stage/src/assets/ibm_db2.svg create mode 100644 debezium-platform-stage/src/pages/Connection/EditConnection.css diff --git a/debezium-platform-stage/CLAUDE.md b/debezium-platform-stage/CLAUDE.md new file mode 100644 index 00000000..320778cb --- /dev/null +++ b/debezium-platform-stage/CLAUDE.md @@ -0,0 +1,255 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Debezium Platform Stage UI is a React + TypeScript single-page application for managing Debezium deployments. It communicates with the [debezium-platform-conductor](https://github.com/debezium/debezium-platform-conductor) backend REST API. + +**Tech Stack:** +- React 19 + TypeScript +- Vite (build tool) +- PatternFly (UI component library) +- React Query (data fetching) +- Jotai (state management) +- React Hook Form + Yup (form handling & validation) +- Vitest (unit testing) +- Cypress (E2E testing) +- MSW (API mocking in tests) +- i18next (internationalization) + +## Development Commands + +```bash +# Install dependencies (Node.js 24.x required) +yarn + +# Start dev server (http://localhost:3000) +yarn dev + +# Run with custom backend URL +CONDUCTOR_URL=http://platform.debezium.io/ yarn dev + +# Production build (outputs to dist/) +yarn build + +# Preview production build +yarn preview + +# Run unit tests (Vitest) +yarn test + +# Generate test coverage report +yarn coverage + +# Run linter +yarn lint + +# Type check +yarn type-check + +# Run E2E tests interactively +yarn e2e + +# Run E2E tests in CI mode (headless) +yarn e2e:ci + +# Open Cypress test runner (requires dev server running) +yarn cypress:open + +# Run all Cypress tests headless +yarn cypress:run +``` + +## Running with Backend + +Start the full development stack (conductor + PostgreSQL): + +```bash +# Using Docker +docker compose up -d + +# Using Podman +podman compose up -d + +# Stop containers +docker compose down # or podman compose down +``` + +Backend API runs on port 8080. PostgreSQL runs on port 5432. Stage UI runs on port 3000. + +## Architecture + +### Directory Structure + +``` +src/ +├── apis/ # API client functions and TypeScript types +├── appLayout/ # App shell (header, sidebar, breadcrumb, notifications) +├── assets/ # Images, icons, static files +├── components/ # Reusable UI components +├── features/ # Feature-specific modules +├── hooks/ # Custom React hooks +├── pages/ # Page components organized by domain: +│ ├── Source/ # Source management pages +│ ├── Destination/ # Destination management pages +│ ├── Pipeline/ # Pipeline management pages +│ ├── Transforms/ # Transform management pages +│ ├── Connection/ # Connection management pages +│ └── Vault/ # Vault management pages +├── stories/ # Storybook stories +├── styles/ # Global styles +├── utils/ # Utility functions and helpers +├── App.tsx # Root component with context providers +├── AppRoutes.tsx # Routes wrapper +├── route.tsx # Route configuration +└── main.tsx # Application entry point +``` + +### Routing + +Routes are centrally defined in `src/route.tsx`. Each route has: +- `path`: URL path +- `component`: Page component +- `navSection`: Groups routes in navigation +- `label`: Navigation label (omit to exclude from sidebar) +- `icon`: Navigation icon (PatternFly icon) +- `title`: Page title + +### Context Providers + +The app uses nested context providers in `App.tsx`: +1. **AppContextProvider** (`appLayout/AppContext.tsx`) - Global app state +2. **NotificationProvider** (`appLayout/AppNotificationContext.tsx`) - Toast notifications +3. **GuidedTourProvider** (`components/GuidedTourContext.tsx`) - User onboarding tours + +### API Layer + +API functions are in `src/apis/apis.tsx`: +- `createPost()` - POST requests for creating resources +- `editPut()` - PUT requests for updating resources +- `fetchData()` - GET requests +- `deleteData()` - DELETE requests +- `fetchDataCall()` - GET with detailed error handling + +Backend URL configuration is in `src/config.ts` via `getBackendUrl()`: +1. Checks `window.__ENV__.CONDUCTOR_URL` (runtime injection for Docker) +2. Falls back to `import.meta.env.CONDUCTOR_URL` (build-time from Vite) +3. Defaults to `http://localhost:8080` + +Vite proxies `/api` requests to the backend URL (configured in `vite.config.ts`). + +### State Management + +- **React Query** (`react-query`) for server state caching and data fetching +- **Jotai** for client-side state atoms +- Context API for app-wide state (navigation, notifications) + +### Form Handling + +Forms use React Hook Form with Yup schema validation. Common pattern: +```tsx +const schema = yup.object().shape({ /* validation rules */ }); +const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: yupResolver(schema) +}); +``` + +## Testing + +### Unit Tests (Vitest) + +- Test files: `*.test.tsx` or `*.test.ts` in `src/` +- Setup files: `vitest.setup.ts` (global), `src/__test__/unit/setup.ts` (test-specific) +- Mocks: `src/__mocks__/` (MSW handlers, component mocks) +- Fixtures: `src/__fixtures__/` (test data) +- Coverage thresholds: 80% for statements, branches, functions, and lines + +**Testing utilities** (`src/__test__/test-utils.tsx`): +- Custom render functions with providers +- Query client setup for React Query tests + +**MSW (Mock Service Worker)** handles API mocking. Server setup in `src/__mocks__/server.ts`. + +**Run single test:** +```bash +yarn test src/components/MyComponent.test.tsx +``` + +### E2E Tests (Cypress) + +- Test files: `cypress/e2e/*.cy.ts` +- Fixtures: `cypress/fixtures/` +- Configuration: `cypress.config.ts` + +**Prerequisites:** Backend must be running (conductor + PostgreSQL): +```bash +cd ../debezium-platform-conductor/dev +docker compose up -d +``` + +Wait 30-60 seconds for services to start, then run tests. + +## Code Patterns + +### PatternFly Components + +Use PatternFly components from `@patternfly/react-core` for consistency: +- `Page`, `PageSection` for layout +- `Card`, `CardBody` for content containers +- `Table` or `@patternfly/react-table` for data tables +- `Form`, `FormGroup` for forms +- `Button`, `Modal`, `Alert`, etc. + +Import PatternFly CSS in components: +```tsx +import '@patternfly/react-core/dist/styles/base.css'; +``` + +### Internationalization + +Use `react-i18next` for all user-facing text: +```tsx +import { useTranslation } from 'react-i18next'; + +const { t } = useTranslation(); +return

{t('page.title')}

; +``` + +Translation files are in `public/locales/`. + +### Type Safety + +All API responses have TypeScript types in `src/apis/apis.tsx`: +- `Source`, `Destination`, `Pipeline`, `Transform`, `Connection`, `Vault` +- `Payload` types for POST/PUT requests +- `ApiResponse` for API function return types + +## Environment Variables + +Set in `.env` file: +- `CONDUCTOR_URL` - Backend API URL (default: `http://localhost:8080`) +- `VITE_PORT` - Dev server port (default: `3000`) + +For Docker deployments, environment variables are injected at runtime via `inject-env.sh` script. + +## Common Tasks + +**Add a new page:** +1. Create component in `src/pages/{Domain}/{PageName}.tsx` +2. Add route to `src/route.tsx` +3. Export from `src/pages/{Domain}/index.ts` + +**Add a new API endpoint:** +1. Add TypeScript types to `src/apis/apis.tsx` or `src/apis/types.tsx` +2. Create API function using `createPost`, `fetchData`, etc. +3. Use with React Query in components + +**Add a new reusable component:** +1. Create in `src/components/{ComponentName}.tsx` +2. Add tests in `src/components/{ComponentName}.test.tsx` +3. Add styles in `src/components/{ComponentName}.css` if needed + +**Update test mocks:** +1. Add MSW handlers to `src/__mocks__/handlers.ts` +2. Add fixture data to `src/__fixtures__/` diff --git a/debezium-platform-stage/src/assets/ibm-db2.png b/debezium-platform-stage/src/assets/ibm-db2.png deleted file mode 100644 index 957b9fe2bdede3e590549f2a30b448f7883efdc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2828 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 diff --git a/debezium-platform-stage/src/assets/ibm_db2.svg b/debezium-platform-stage/src/assets/ibm_db2.svg new file mode 100644 index 00000000..e291a729 --- /dev/null +++ b/debezium-platform-stage/src/assets/ibm_db2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/debezium-platform-stage/src/components/ComponentImage.tsx b/debezium-platform-stage/src/components/ComponentImage.tsx index faf2c706..b5057d34 100644 --- a/debezium-platform-stage/src/components/ComponentImage.tsx +++ b/debezium-platform-stage/src/components/ComponentImage.tsx @@ -29,7 +29,7 @@ import milvus from "../assets/milvus.png"; import qdrant from "../assets/qdrant.png"; import redis from "../assets/redis.png"; import http from "../assets/http2.png"; -import db2 from "../assets/ibm-db2.png"; +import db2 from "../assets/ibm_db2.svg"; interface ConnectorImageProps { connectorType: string; diff --git a/debezium-platform-stage/src/components/SourceDestinationSelectionList.tsx b/debezium-platform-stage/src/components/SourceDestinationSelectionList.tsx index ac5e8bf8..2b88cf0b 100644 --- a/debezium-platform-stage/src/components/SourceDestinationSelectionList.tsx +++ b/debezium-platform-stage/src/components/SourceDestinationSelectionList.tsx @@ -5,9 +5,8 @@ import { EmptyStateVariant, Flex, FlexItem, - Label, } from "@patternfly/react-core"; -import { DataSinkIcon, DataSourceIcon, TagIcon } from "@patternfly/react-icons"; +import { DataSinkIcon, DataSourceIcon } from "@patternfly/react-icons"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import React from "react"; import { @@ -22,8 +21,8 @@ import { getConnectorTypeName } from "../utils/helpers"; import ConnectorImage from "./ComponentImage"; import { API_URL } from "../utils/constants"; import { useQuery } from "react-query"; -import { getActivePipelineCount } from "../utils/pipelineUtils"; import { useTranslation } from "react-i18next"; +import UsedIn from "./UsedIn"; interface ISourceDestinationSelectionListProps { tableType: "source" | "destination"; @@ -80,13 +79,7 @@ const SourceDestinationSelectionList: React.FunctionComponent< - + ))} @@ -95,12 +88,12 @@ const SourceDestinationSelectionList: React.FunctionComponent< ) : ( - {t("emptyState.description", {val: tableType})} + {t("emptyState.description", { val: tableType })} {/* diff --git a/debezium-platform-stage/src/pages/Connection/CreateConnection.tsx b/debezium-platform-stage/src/pages/Connection/CreateConnection.tsx index b34b6068..5dd2e905 100644 --- a/debezium-platform-stage/src/pages/Connection/CreateConnection.tsx +++ b/debezium-platform-stage/src/pages/Connection/CreateConnection.tsx @@ -221,15 +221,15 @@ const CreateConnection: React.FunctionComponent = ({ sel return selectedSchema ? ({ - type: selectedSchema?.type.toUpperCase() || extractConnectorType(connectionId || "").toUpperCase(), - config: mergedConfig, - name: name as string, - } as ConnectionPayload) + type: selectedSchema?.type.toUpperCase() || extractConnectorType(connectionId || "").toUpperCase(), + config: mergedConfig, + name: name as string, + } as ConnectionPayload) : { - type: extractConnectorType(connectionId || "").toUpperCase(), - config: validation.additionalFlat, - name: name as string, - }; + type: extractConnectorType(connectionId || "").toUpperCase(), + config: validation.additionalFlat, + name: name as string, + }; }; const handleValidateFromForm = (data: ConnectionFormValues) => { @@ -439,7 +439,7 @@ const CreateConnection: React.FunctionComponent = ({ sel header={ {`Additional properties`}, + text: {selectedSchemaProperties ? `Additional properties` : `Configuration properties`}, id: `field-group-${connectionId}-properties-id`, }} titleDescription={t("form.subHeading.description")} diff --git a/debezium-platform-stage/src/pages/Connection/EditConnection.css b/debezium-platform-stage/src/pages/Connection/EditConnection.css new file mode 100644 index 00000000..641c52e5 --- /dev/null +++ b/debezium-platform-stage/src/pages/Connection/EditConnection.css @@ -0,0 +1,14 @@ +/* Review value styling for view mode */ +.source-schema-review__value { + display: inline-block; +} + +.source-schema-review__value--empty { + color: var(--pf-v6-global--Color--200, #6a6e73); + font-style: italic; +} + +.source-schema-review__value--set { + color: var(--pf-v6-global--Color--100, #151515); + font-weight: 500; +} diff --git a/debezium-platform-stage/src/pages/Connection/EditConnection.tsx b/debezium-platform-stage/src/pages/Connection/EditConnection.tsx index cd048cab..ce50a707 100644 --- a/debezium-platform-stage/src/pages/Connection/EditConnection.tsx +++ b/debezium-platform-stage/src/pages/Connection/EditConnection.tsx @@ -29,6 +29,30 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useTranslation } from "react-i18next"; import { PageHeader } from "@patternfly/react-component-groups"; +const EMPTY_DISPLAY = "—"; + +function reviewValue(raw: string | number | undefined): string { + if (raw === undefined || raw === null) return EMPTY_DISPLAY; + const strValue = String(raw); + return strValue.trim() === "" ? EMPTY_DISPLAY : strValue; +} + +const ReviewValueSpan: React.FC<{ raw: string | number | undefined }> = ({ raw }) => { + const text = reviewValue(raw); + const unset = text === EMPTY_DISPLAY; + return ( + + {text} + + ); +}; + export interface IEditConnectionProps { sampleProp?: string; } @@ -406,7 +430,7 @@ const EditConnection: React.FunctionComponent = () => { name={"name"} control={control} rules={{ required: true }} - render={({ field }) => } + render={({ field }) => viewMode ? : } /> {(!viewMode && errors.name) && ( @@ -458,25 +482,26 @@ const EditConnection: React.FunctionComponent = () => { name={propertyName} rules={{ required: selectedSchema?.schema?.required.includes(propertyName) }} control={control} - render={({ field }) => } + render={({ field }) => viewMode ? : } />} {propertySchema.type === "list" && } + render={({ field }) => viewMode ? : } />} {propertySchema.type === "integer" && ( + render={({ field }) => viewMode ? ( + + ) : ( field.onChange(value === '' ? '' : Number(value))} /> )} @@ -509,7 +534,7 @@ const EditConnection: React.FunctionComponent = () => { header={ {t("form.subHeading.title")}, + text: {selectedSchema ? `Additional properties` : `Configuration properties`}, id: `field-group-${connectionId}-id`, }} titleDescription={!viewMode ? t("form.subHeading.description") : undefined} @@ -545,7 +570,6 @@ const EditConnection: React.FunctionComponent = () => { - {!viewMode && ( From 59f67b52085264d3f671528e0f54482a9f841916 Mon Sep 17 00:00:00 2001 From: indraraj Date: Fri, 29 May 2026 19:27:12 +0530 Subject: [PATCH 2/5] debezium/dbz#1978 Update the breadcrumb and to do collection call in source only Signed-off-by: indraraj --- .../public/locales/en/common.json | 17 +++- .../public/locales/it/common.json | 17 +++- .../src/appLayout/AppBreadcrumb.tsx | 58 +++++------ .../src/components/CreateSchemaForm.tsx | 98 ++++++++++--------- .../pages/Destination/CreateDestination.tsx | 2 +- .../src/pages/Destination/EditDestination.tsx | 2 +- 6 files changed, 117 insertions(+), 77 deletions(-) diff --git a/debezium-platform-stage/public/locales/en/common.json b/debezium-platform-stage/public/locales/en/common.json index f95abbf2..d09b2787 100644 --- a/debezium-platform-stage/public/locales/en/common.json +++ b/debezium-platform-stage/public/locales/en/common.json @@ -102,5 +102,20 @@ "usedIn": "Used in", "replayTour": "Replay Guided Tour", "vaults": "Vaults", - "verify": "Verify" + "verify": "Verify", + "connector": { + "jumplinks": { + "connectorEssentials": "Connector Essentials", + "additionalProperties": "Additional Properties" + }, + "form": { + "validationFailedGeneric": "Please fill all required fields.", + "validationFailedInOneSection": "Please fill all required fields in {{section}} section.", + "validationFailedInMultipleSections": "Please fill all required fields in {{list}} sections." + }, + "create": { + "dataTableDescription": "Select the {{val}} to be targeted for sync", + "dataTableTitle": "{{val}} database" + } + } } \ No newline at end of file diff --git a/debezium-platform-stage/public/locales/it/common.json b/debezium-platform-stage/public/locales/it/common.json index 1d82425f..9322dfd8 100644 --- a/debezium-platform-stage/public/locales/it/common.json +++ b/debezium-platform-stage/public/locales/it/common.json @@ -101,5 +101,20 @@ "type": "Tipo", "replayTour": "Ripeti il Tour Guidato", "vaults": "Vault", - "verify": "Verifica" + "verify": "Verifica", + "connector": { + "jumplinks": { + "connectorEssentials": "Elementi essenziali del connettore", + "additionalProperties": "Proprietà aggiuntive" + }, + "form": { + "validationFailedGeneric": "Compila tutti i campi obbligatori.", + "validationFailedInOneSection": "Compila tutti i campi obbligatori nella sezione {{section}}.", + "validationFailedInMultipleSections": "Compila tutti i campi obbligatori nelle sezioni {{list}}." + }, + "create": { + "dataTableDescription": "Seleziona il {{val}} da sincronizzare", + "dataTableTitle": "{{val}} database" + } + } } \ No newline at end of file diff --git a/debezium-platform-stage/src/appLayout/AppBreadcrumb.tsx b/debezium-platform-stage/src/appLayout/AppBreadcrumb.tsx index cdb6db79..dfa75a9e 100644 --- a/debezium-platform-stage/src/appLayout/AppBreadcrumb.tsx +++ b/debezium-platform-stage/src/appLayout/AppBreadcrumb.tsx @@ -42,14 +42,15 @@ const AppBreadcrumb: React.FC = () => { {generateBreadcrumbItem("#", "Catalog", navigate, true)} ); - case route.match("source/[^/]+") !== null: + case route.includes("/source/create_source"): return ( {generateBreadcrumbItem("/source", "Source", navigate)} - {generateBreadcrumbItem("#", "Edit source", navigate, true)} + {generateBreadcrumbItem("/source/catalog", "Catalog", navigate)} + {generateBreadcrumbItem("#", "Create source", navigate, true)} ); - case route.includes("/source/create_source"): + case route.match("source/[^/]+") !== null: return ( {generateBreadcrumbItem("/source", "Source", navigate)} @@ -64,14 +65,15 @@ const AppBreadcrumb: React.FC = () => { {generateBreadcrumbItem("#", "Catalog", navigate, true)} ); - case route.match("destination/[^/]+") !== null: + case route.includes("/destination/create_destination"): return ( {generateBreadcrumbItem("/destination", "Destination", navigate)} - {generateBreadcrumbItem("#", "Edit destination", navigate, true)} + {generateBreadcrumbItem("/destination/catalog", "Catalog", navigate)} + {generateBreadcrumbItem("#", "Create destination", navigate, true)} ); - case route.includes("/destination/create_destination"): + case route.match("destination/[^/]+") !== null: return ( {generateBreadcrumbItem("/destination", "Destination", navigate)} @@ -101,27 +103,12 @@ const AppBreadcrumb: React.FC = () => { {generateBreadcrumbItem("#", "Pipeline designer", navigate, true)} ); - case route.match("/pipeline/[^/]+") !== null: - return ( - - {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} - {generateBreadcrumbItem("#", "Overview", navigate, true)} - - ); - case route.match("/pipeline/pipeline_edit/[^/]+") !== null: - return ( - - {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} - {generateBreadcrumbItem("#", "indra-ui-test", navigate, true)} - {generateBreadcrumbItem("#", "Edit", navigate, true)} - - ); case route === "/pipeline/pipeline_designer/configure": return ( {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} - {generateBreadcrumbItem("#", "Pipeline designer", navigate)} - {generateBreadcrumbItem("#", "Create pipeline", navigate)} + {generateBreadcrumbItem("/pipeline/pipeline_designer", "Pipeline designer", navigate)} + {generateBreadcrumbItem("#", "Create pipeline", navigate, true)} ); case route === "/pipeline/pipeline_designer/destination": @@ -130,10 +117,10 @@ const AppBreadcrumb: React.FC = () => { {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} {generateBreadcrumbItem( "/pipeline/pipeline_designer", - " Pipeline designer", + "Pipeline designer", navigate )} - {generateBreadcrumbItem("#", "Destination", navigate, true)} + {generateBreadcrumbItem("#", "Create pipeline", navigate, true)} ); case route.includes( @@ -147,12 +134,27 @@ const AppBreadcrumb: React.FC = () => { "Pipeline designer", navigate )} + {generateBreadcrumbItem("#", "Create pipeline", navigate, true)} + + ); + case route.match("/pipeline/pipeline_edit/[^/]+") !== null: + return ( + + {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} + {generateBreadcrumbItem("#", "indra-ui-test", navigate, true)} + {generateBreadcrumbItem("#", "Edit", navigate, true)} + + ); + case route.match("/pipeline/[^/]+") !== null: + return ( + + {generateBreadcrumbItem("/pipeline", "Pipeline", navigate)} {generateBreadcrumbItem( - "pipeline/pipeline_designer/destination", - "Destination", + "/pipeline/pipeline_designer", + "Pipeline designer", navigate )} - {generateBreadcrumbItem("#", "Create destination", navigate, true)} + {generateBreadcrumbItem("#", "Create pipeline", navigate, true)} ); default: diff --git a/debezium-platform-stage/src/components/CreateSchemaForm.tsx b/debezium-platform-stage/src/components/CreateSchemaForm.tsx index 57fe673d..079c2e40 100644 --- a/debezium-platform-stage/src/components/CreateSchemaForm.tsx +++ b/debezium-platform-stage/src/components/CreateSchemaForm.tsx @@ -57,6 +57,7 @@ import { useQuery } from "react-query"; import { Connection, ConnectionConfig, + Destination, fetchData, fetchDataCall, Source, @@ -108,10 +109,11 @@ interface connectionsList extends Connection { interface CreateSchemaFormProps { connectorSchema: ConnectorSchema; - sourceId: string; + sourceId?: string; + destinationId?: string; dataType?: string; onSubmit: (payload: Record) => void; - initialSource?: Source; + initialSource?: Source | Destination; readOnly?: boolean; /** Initial layout; user can still switch via the toggle. Pipeline designer modal uses "tabs". */ defaultLayoutMode?: "jumplinks" | "tabs"; @@ -149,7 +151,7 @@ function getFirstJumplinkValidationErrorElementId( orderedGroups: SchemaGroup[], groupedProperties: Map ): string | null { - if (newErrors["source-name"]) return "source-name"; + if (newErrors["connector-name"]) return "connector-name"; if (newErrors.connection) return "conn-typeahead-select"; for (const group of orderedGroups) { @@ -198,8 +200,8 @@ function collectValidationErrorSectionLabels( } }; - if (newErrors["source-name"] || newErrors.connection) { - push(t("source:jumplinks.connectorEssentials")); + if (newErrors["connector-name"] || newErrors.connection) { + push(t("connector:jumplinks.connectorEssentials")); } for (const group of orderedGroups) { @@ -211,7 +213,7 @@ function collectValidationErrorSectionLabels( } if (additionalErrorRowIds.size > 0) { - push(t("source:jumplinks.additionalProperties")); + push(t("connector:jumplinks.additionalProperties")); } return names; @@ -219,27 +221,33 @@ function collectValidationErrorSectionLabels( function formatValidationFailureNotificationBody(sections: string[], t: TFunction): string { if (sections.length === 0) { - return t("source:form.validationFailedGeneric"); + return t("connector:form.validationFailedGeneric"); } if (sections.length === 1) { - return t("source:form.validationFailedInOneSection", { section: sections[0] }); + return t("connector:form.validationFailedInOneSection", { section: sections[0] }); } - return t("source:form.validationFailedInMultipleSections", { list: sections.join(", ") }); + return t("connector:form.validationFailedInMultipleSections", { list: sections.join(", ") }); } const CreateSchemaForm = React.forwardRef< CreateSchemaFormHandle, CreateSchemaFormProps ->(({ connectorSchema, sourceId, dataType, onSubmit, initialSource, readOnly = false, defaultLayoutMode = "jumplinks", hideSignalCollections = false }, ref) => { +>(({ connectorSchema, sourceId, destinationId, dataType, onSubmit, initialSource, readOnly = false, defaultLayoutMode = "jumplinks", hideSignalCollections = false }, ref) => { const { t } = useTranslation(); const { addNotification } = useNotification(); const hydratedSourceIdRef = useRef(null); const lastValidationFailureBodyRef = useRef(""); + // Use either sourceId or destinationId + const connectorId = sourceId || destinationId || ""; + // Determine if this is a destination or source context + const isDestination = !!destinationId; + const connectorTypeLabel = isDestination ? "Destination" : "Source"; + // Layout toggle const [layoutMode, setLayoutMode] = useState<"jumplinks" | "tabs">(defaultLayoutMode); - const [sourceName, setSourceName] = useState(""); + const [connectorName, setConnectorName] = useState(""); const [description, setDescription] = useState(""); const [schemaValues, setSchemaValues] = useState>({}); @@ -310,7 +318,7 @@ const CreateSchemaForm = React.forwardRef< [connectorSchema.properties] ); - const connectorTypeString = dataType || sourceId; + const connectorTypeString = dataType || connectorId; const tableManagedFilterNames = useMemo( () => new Set(getTableManagedFilterPropertyNames(connectorTypeString)), @@ -338,7 +346,7 @@ const CreateSchemaForm = React.forwardRef< ); /* eslint-disable react-hooks/set-state-in-effect */ - setSourceName(initialSource.name); + setConnectorName(initialSource.name); setDescription(initialSource.description ?? ""); setSelectedConnection(initialSource.connection); setConnectionInputValue(initialSource.connection?.name ?? ""); @@ -464,7 +472,7 @@ const CreateSchemaForm = React.forwardRef< return response.data as TableData; }, { - enabled: selectedConnectionId != null, + enabled: selectedConnectionId != null && !isDestination, } ); @@ -504,12 +512,12 @@ const CreateSchemaForm = React.forwardRef< id="field-group-data-table-id" className="table-explorer-section__title" > - {t("source:create.dataTableTitle", { + {t("connector:create.dataTableTitle", { val: getConnectorTypeName(connectorTypeString), })} - {t("source:create.dataTableDescription", { + {t("connector:create.dataTableDescription", { val: getDataExplorerScopePhrase(connectorTypeString), })} @@ -534,8 +542,8 @@ const CreateSchemaForm = React.forwardRef< ]); const baseSelectOptions = useMemo( - () => getInitialSelectOptions(connections, dataType || sourceId), - [connections, dataType, sourceId] + () => getInitialSelectOptions(connections, dataType || connectorId), + [connections, dataType, connectorId] ); const selectOptions = useMemo(() => { @@ -678,7 +686,7 @@ const CreateSchemaForm = React.forwardRef< return; } const payload = { - databaseType: getDatabaseType(sourceId), + databaseType: getDatabaseType(connectorId), connectionId: selectedConnection.id, fullyQualifiedTableName: signalCollectionNameVerify, }; @@ -741,7 +749,7 @@ const CreateSchemaForm = React.forwardRef< const validate = useCallback((): boolean => { if (readOnly) return true; const newErrors: Record = {}; - if (!sourceName.trim()) newErrors["source-name"] = t("statusMessage:smartEditor.sourceNameRequired", "Source name is required"); + if (!connectorName.trim()) newErrors["connector-name"] = t("statusMessage:smartEditor.connectorNameRequired", `${connectorTypeLabel} name is required`); if (!selectedConnection) newErrors.connection = t("statusMessage:smartEditor.connectionRequired", "Connection is required"); for (const prop of connectorSchema.properties) { @@ -803,7 +811,7 @@ const CreateSchemaForm = React.forwardRef< }, [ readOnly, layoutMode, - sourceName, + connectorName, selectedConnection, schemaValues, connectorSchema.properties, @@ -816,7 +824,7 @@ const CreateSchemaForm = React.forwardRef< ]); const getLastValidationFailureBody = useCallback( - () => lastValidationFailureBodyRef.current || t("source:form.validationFailedGeneric"), + () => lastValidationFailureBodyRef.current || t("connector:form.validationFailedGeneric"), [t] ); @@ -844,9 +852,9 @@ const CreateSchemaForm = React.forwardRef< Object.assign(config, getIncludeList(selectedDataListItems, connectorTypeString)); const payload: Record = { - name: sourceName, + name: connectorName, description, - type: initialSource?.type ?? sourceId, + type: initialSource?.type ?? connectorId, schema: initialSource?.schema ?? "schema321", vaults: initialSource?.vaults ?? [], ...(selectedConnection ? { connection: selectedConnection } : {}), @@ -866,9 +874,9 @@ const CreateSchemaForm = React.forwardRef< additionalProps, signalCollectionName, selectedDataListItems, - sourceName, + connectorName, description, - sourceId, + connectorId, selectedConnection, onSubmit, initialSource, @@ -884,43 +892,43 @@ const CreateSchemaForm = React.forwardRef< const renderConnectorEssentials = () => (
- + - + - {getConnectorTypeName(dataType || sourceId)} + {getConnectorTypeName(dataType || connectorId)} - + { - setSourceName(val); - setErrors((e) => ({ ...e, "source-name": undefined })); + setConnectorName(val); + setErrors((e) => ({ ...e, "connector-name": undefined })); }} - validated={errors["source-name"] ? "error" : "default"} + validated={errors["connector-name"] ? "error" : "default"} readOnly={readOnly} {...(readOnly ? { readOnlyVariant: "plain" as const } : {})} /> - {errors["source-name"] && ( + {errors["connector-name"] && ( } variant="error"> - {errors["source-name"]} + {errors["connector-name"]} )} - + setDescription(val)} readOnly={readOnly} @@ -928,12 +936,12 @@ const CreateSchemaForm = React.forwardRef< /> - {t("form.field.description.helper", { val: "source" })} + {t("form.field.description.helper", { val: isDestination ? "destination" : "source" })} - +