diff --git a/debezium-platform-stage/src/App.tsx b/debezium-platform-stage/src/App.tsx index ad61e61..f66bae2 100644 --- a/debezium-platform-stage/src/App.tsx +++ b/debezium-platform-stage/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter as Router } from "react-router-dom"; import "@patternfly/react-core/dist/styles/base.css"; +import '@patternfly/react-styles/css/utilities/Spacing/spacing.css'; import { AppLayout } from "./appLayout/AppLayout"; import { AppRoutes } from "./AppRoutes"; import { AppContextProvider } from "./appLayout/AppContext"; diff --git a/debezium-platform-stage/src/__mocks__/data/Predicates.json b/debezium-platform-stage/src/__mocks__/data/Predicates.json new file mode 100644 index 0000000..869a775 --- /dev/null +++ b/debezium-platform-stage/src/__mocks__/data/Predicates.json @@ -0,0 +1,28 @@ +[ + { + "properties": { + "pattern": { + "title": "Pattern", + "description": "A predicate which is true for records with a topic name that matches the configured regular expression.", + "type": "STRING", + "x-name": "pattern" + } + }, + "predicate": "org.apache.kafka.connect.transforms.predicates.TopicNameMatches" + }, + { + "properties": { + "name": { + "title": "Name", + "description": "A predicate which is true for records with at least one header with the configured name.", + "type": "STRING", + "x-name": "name" + } + }, + "predicate": "org.apache.kafka.connect.transforms.predicates.HasHeaderKey" + }, + { + "properties": {}, + "predicate": "org.apache.kafka.connect.transforms.predicates.RecordIsTombstone" + } + ] \ No newline at end of file diff --git a/debezium-platform-stage/src/apis/apis.tsx b/debezium-platform-stage/src/apis/apis.tsx index 8b86628..f6ae514 100644 --- a/debezium-platform-stage/src/apis/apis.tsx +++ b/debezium-platform-stage/src/apis/apis.tsx @@ -67,12 +67,19 @@ export type Source = { id: number; }; +export type Predicate = { + type: string; + config: Record; + negate?: boolean; +} + export type TransformData = { type: string; schema: string; vaults: Vault[]; config: SourceConfig; description?: string; + predicate?: Predicate; name: string; id: number; }; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/CreationFlow.tsx b/debezium-platform-stage/src/components/pipelineDesigner/CreationFlow.tsx deleted file mode 100644 index 05035f7..0000000 --- a/debezium-platform-stage/src/components/pipelineDesigner/CreationFlow.tsx +++ /dev/null @@ -1,398 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { useCallback, useMemo, useState } from "react"; -import ReactFlow, { - applyNodeChanges, - applyEdgeChanges, - addEdge, - NodeChange, - Node, - EdgeChange, - Edge, - Connection, - Background, -} from "reactflow"; -import AddTransformationNode from "./TransformAdditionNode"; - -import DataNode from "./DataNode"; - -import { MdLogin, MdLogout } from "react-icons/md"; -import DataNodeSelector from "./DataSelectorNode"; -import { Button, Modal, ModalBody, ModalHeader } from "@patternfly/react-core"; -import { Destination, Source } from "../../apis/apis"; -import { PlusIcon } from "@patternfly/react-icons"; -import "./CreationFlow.css"; -import SourcePipelineModel from "./SourcePipelineModel"; -import DestinationPipelineModel from "./DestinationPipelineModel"; -import CustomEdgeDestination from "./CustomEdgeDestination"; -import CustomEdgeSource from "./CustomEdgeSource"; -import { useData } from "../../appLayout/AppContext"; -import { AppColors } from "@utils/constants"; - -const nodeTypes = { - dataNodeSelector: DataNodeSelector, - addTransformation: AddTransformationNode, - dataNode: DataNode, -}; - -const edgeTypes = { - customEdgeSource: CustomEdgeSource, - customEdgeDestination: CustomEdgeDestination, -}; - -// const defaultEdgeOptions = { -// type: "customEdge", -// markerEnd: "edge-circle", -// }; - -const proOptions = { hideAttribution: true }; - -interface CreationFlowProps { - updateIfSourceConfigured: (isConfigured: boolean) => void; - updateIfDestinationConfigured: (isConfigured: boolean) => void; - isSourceConfigured: boolean; - isDestinationConfigured: boolean; - updateSelectedSource: (source: Source) => void; - updateSelectedDestination: (destination: Destination) => void; -} - -const CreationFlow: React.FC = ({ - updateIfSourceConfigured, - updateIfDestinationConfigured, - isSourceConfigured, - isDestinationConfigured, - updateSelectedSource, - updateSelectedDestination, -}) => { - - const { darkMode } = useData(); - - const [updatedSourceNodes, setUpdatedSourceNodes] = useState(); - const [updatedDestinationNodes, setUpdatedDestinationNodes] = useState(); - - const [isSourceModalOpen, setIsSourceModalOpen] = useState(false); - const [isDestinationModalOpen, setIsDestinationModalOpen] = useState(false); - - const handleSourceModalToggle = useCallback(() => { - setIsSourceModalOpen(!isSourceModalOpen); - }, [isSourceModalOpen]); - - const handleDestinationModalToggle = useCallback(() => { - setIsDestinationModalOpen(!isDestinationModalOpen); - }, [isDestinationModalOpen]); - - const defaultSourceNode = useMemo(() => { - return { - id: "source", - data: { - icon: MdLogout, - label: "Source", - type: "source", - action: ( - - ), - }, - position: { x: 100, y: 150 }, - type: "dataNodeSelector", - // draggable: false, - }; - }, [handleSourceModalToggle]); - - const transformationGroup = useMemo(() => { - return { - id: "transformation_group", - data: { label: "Group B.A" }, - position: { x: 330, y: 100 }, - style: { - boxShadow: - "7px 7px 15px rgba(88, 178, 218, 0.3), -7px -7px 15px rgba(165, 200, 45, 0.3)", - borderRadius: "10px", - border: "0", - width: 210, - height: 150, - }, - type: "group", - // draggable: false, - }; - }, []); - - const defaultTransformationNode = useMemo(() => { - return { - id: "add_transformation", - data: { - label: "Transforms", - sourcePosition: "right", - targetPosition: "left", - }, - position: { x: 65, y: 35 }, - targetPosition: "left", - type: "addTransformation", - parentId: "transformation_group", - extent: "parent", - // draggable: false, - }; - }, []); - - const defaultDestinationNode = useMemo(() => { - return { - id: "destination", - data: { - icon: MdLogin, - label: "Destination", - type: "destination", - action: ( - - ), - }, - position: { x: 650, y: 150 }, - type: "dataNodeSelector", - // draggable: false, - }; - }, [handleDestinationModalToggle]); - - const initialNodes = [ - defaultSourceNode, - transformationGroup, - defaultTransformationNode, - defaultDestinationNode, - ]; - - const initialEdges = [ - { - id: "source-add_transformation", - source: "source", - target: "add_transformation", - // animated: true, - type: "customEdgeSource", - sourceHandle: "a", - }, - { - id: "add_transformation-destination", - source: "add_transformation", - target: "destination", - // animated: true, - type: "customEdgeDestination", - }, - ]; - - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(initialEdges); - - const onNodesChange = useCallback( - (changes: NodeChange[]) => - setNodes((nds: Node[]) => applyNodeChanges(changes, nds)), - [setNodes] - ); - const onEdgesChange = useCallback( - (changes: EdgeChange[]) => - setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds)), - [setEdges] - ); - const onConnect = useCallback( - (connection: Connection) => { - setEdges((eds: Edge[]) => addEdge(connection, eds)); - }, - [setEdges] - ); - - const onSourceSelection = useCallback( - (source: Source) => { - const selectedSourceNode = { - id: "source", - data: { - connectorType: source.type, - label: source.name, - type: "source", - // draggable: false, - editAction: () => setIsSourceModalOpen(true), - }, - position: { x: 100, y: 160 }, - type: "dataNode", - // draggable: false, - }; - updateIfSourceConfigured(true); - setUpdatedSourceNodes(selectedSourceNode); - updateSelectedSource(source); - - const destinationNode = isDestinationConfigured - ? updatedDestinationNodes - : defaultDestinationNode; - - // Update the nodes - setNodes([ - selectedSourceNode, - transformationGroup, - defaultTransformationNode, - destinationNode, - ]); - - handleSourceModalToggle(); - }, - [ - transformationGroup, - defaultTransformationNode, - defaultDestinationNode, - updateIfSourceConfigured, - isDestinationConfigured, - updatedDestinationNodes, - updateSelectedSource, - handleSourceModalToggle, - ] - ); - - const onDestinationSelection = useCallback( - (destination: Destination) => { - const selectedDestinationNode = { - id: "destination", - data: { - connectorType: destination.type, - label: destination.name, - type: "destination", - // draggable: false, - editAction: () => setIsDestinationModalOpen(true), - }, - position: { x: 650, y: 160 }, - type: "dataNode", - // draggable: false, - }; - - updateIfDestinationConfigured(true); - setUpdatedDestinationNodes(selectedDestinationNode); - updateSelectedDestination(destination); - - const sourceNode = isSourceConfigured - ? updatedSourceNodes - : defaultSourceNode; - - // Update the nodes - setNodes([ - sourceNode, - transformationGroup, - defaultTransformationNode, - selectedDestinationNode, - ]); - - handleDestinationModalToggle(); - }, - [ - transformationGroup, - defaultTransformationNode, - defaultSourceNode, - updateIfDestinationConfigured, - isSourceConfigured, - updatedSourceNodes, - updateSelectedDestination, - handleDestinationModalToggle, - ] - ); - - return ( - <> - - - - - - - - - - - - - - {/* - - */} - - - - - - - - - - - - - - - - - - - - ); -}; - -export default CreationFlow; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/CreationFlowTransform.tsx b/debezium-platform-stage/src/components/pipelineDesigner/CreationFlowTransform.tsx index d964954..1b26b8a 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/CreationFlowTransform.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/CreationFlowTransform.tsx @@ -12,6 +12,7 @@ import ReactFlow, { Background, MiniMap, PanOnScrollMode, + useReactFlow, } from "reactflow"; import TransformAdditionNode from "./TransformAdditionNode"; @@ -20,19 +21,25 @@ import DataNode from "./DataNode"; import { MdLogin, MdLogout } from "react-icons/md"; import DataSelectorNode from "./DataSelectorNode"; import { Button, Modal, ModalBody, ModalHeader } from "@patternfly/react-core"; -import { Destination, Source, Transform, TransformData } from "../../apis/apis"; +import { + Destination, + Predicate, + Source, + Transform, + TransformData, +} from "../../apis/apis"; import { PlusIcon } from "@patternfly/react-icons"; import "./CreationFlow.css"; -import SourcePipelineModel from "./SourcePipelineModel"; -import DestinationPipelineModel from "./DestinationPipelineModel"; +import PipelineSourceModel from "./PipelineSourceModel"; +import PipelineDestinationModel from "./PipelineDestinationModel"; import { useData } from "../../appLayout/AppContext"; import { AppColors } from "@utils/constants"; import TransformLinkNode from "./TransformLinkNode"; -import TransformPipelineModel from "./TransformPipelineModel"; +import PipelineTransformModel from "./PipelineTransformModel"; import TransformGroupNode from "./TransformGroupNode"; import TransformSelectorNode from "./TransformSelectorNode"; -import TransformSelectedNode from "./TransformSelectedNode"; +import TransformCollapsedNode from "./TransformCollapsedNode"; import UnifiedCustomEdge from "./UnifiedCustomEdge"; import UnifiedMultiEdge from "./UnifiedMultiEdge"; @@ -42,7 +49,7 @@ const nodeTypes = { addTransformNode: TransformAdditionNode, transformGroupNode: TransformGroupNode, transformSelectorNode: TransformSelectorNode, - transformSelectedNode: TransformSelectedNode, + transformCollapsedNode: TransformCollapsedNode, dataNode: DataNode, }; @@ -76,6 +83,30 @@ const CreationFlowTransform: React.FC = ({ rearrangeTrigger, }) => { const { darkMode } = useData(); + + const reactFlowInstance = useReactFlow(); + + const refitElements = () => { + setTimeout(() => { + reactFlowInstance.fitView({ + padding: 0.2, // 20% padding + duration: 200, // 200ms + }); + }, 50); + }; + + const reactFlowWrapper = useRef(null); + + useEffect(() => { + const handleResize = () => { + refitElements(); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + const [isSourceModalOpen, setIsSourceModalOpen] = useState(false); const [isTransformModalOpen, setIsTransformModalOpen] = useState(false); const [isDestinationModalOpen, setIsDestinationModalOpen] = useState(false); @@ -104,7 +135,7 @@ const CreationFlowTransform: React.FC = ({ ? handleDestinationModalToggle : handleTransformModalToggle } - style={{ paddingRight: 10, paddingLeft: 10, fontSize: ".8em" }} + style={{ paddingRight: 5, paddingLeft: 5, fontSize: ".8em" }} icon={} size="sm" > @@ -235,7 +266,8 @@ const CreationFlowTransform: React.FC = ({ const createNewTransformNode = ( id: string, xPosition: number, - transformName: string + transformName: string, + transformPredicate?: Predicate ) => { return { id, @@ -243,6 +275,12 @@ const CreationFlowTransform: React.FC = ({ label: transformName, sourcePosition: "left", targetPosition: "right", + ...(transformPredicate?.type && { + predicate: { + label: transformPredicate.type.split(".").pop(), + negate: transformPredicate.negate, + }, + }), }, position: { x: xPosition, y: 31 }, targetPosition: "left", @@ -252,6 +290,7 @@ const CreationFlowTransform: React.FC = ({ type: "transformLinkNode", parentId: "transform_group", extent: "parent", + draggable: false, }; }; const selectedTransformRef = useRef(selectedTransform); @@ -376,7 +415,7 @@ const CreationFlowTransform: React.FC = ({ setEdges([...newEdge]); }, [cardButtonTransform, onToggleDrawer]); - const TransformSelectedNode = useMemo(() => { + const TransformCollapsedNode = useMemo(() => { return { id: "transform_selected", data: { @@ -388,7 +427,7 @@ const CreationFlowTransform: React.FC = ({ }, position: { x: 270, y: 78 }, targetPosition: "left", - type: "transformSelectedNode", + type: "transformCollapsedNode", draggable: false, }; }, [handleExpand]); @@ -419,7 +458,7 @@ const CreationFlowTransform: React.FC = ({ (node: any) => !node.id.includes("transform") && node.id !== "destination" ), - TransformSelectedNode, + TransformCollapsedNode, updatedDataSelectorDestinationNode, ]; }); @@ -434,7 +473,7 @@ const CreationFlowTransform: React.FC = ({ }, ]); }, [ - TransformSelectedNode, + TransformCollapsedNode, dataSelectorDestinationNode, isDestinationConfiguredRef, ]); @@ -499,7 +538,8 @@ const CreationFlowTransform: React.FC = ({ const newTransformNode = createNewTransformNode( newId, xPosition, - transform.name + transform.name, + transform.predicate ); setNodes((prevNodes: any) => { @@ -640,43 +680,48 @@ const CreationFlowTransform: React.FC = ({ ); return ( <> - - - {/* */} - + { + instance.fitView({ padding: 0.2 }); }} - gap={13} - color={darkMode ? AppColors.dark : AppColors.white} - /> - - - - - - - - - - + > + + + + + + + + + + + + + + = ({ description="Select a source to be used in pipeline from the list of already configured source listed below or configure a new source by selecting create a new source radio card." /> - + = ({ description="Select a source to be used in pipeline from the list of already configured source listed below or configure a new source by selecting create a new source radio card." /> - + = ({ tabIndex={0} id="modal-box-body-destination-with-description" > - diff --git a/debezium-platform-stage/src/components/pipelineDesigner/DataNode.tsx b/debezium-platform-stage/src/components/pipelineDesigner/DataNode.tsx index 10b2e43..50e9e06 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/DataNode.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/DataNode.tsx @@ -1,11 +1,11 @@ import { Card, CardBody, - Bullseye, Stack, StackItem, Tooltip, Content, + Divider, } from "@patternfly/react-core"; import { Handle, Position } from "reactflow"; import ConnectorImage from "../ComponentImage"; @@ -29,31 +29,33 @@ const DataNode: React.FC = ({ data }) => { return ( <> - {!data.compositionFlow && (
- Change pipeline {data.type}.
}> -
- -
- - )} - + {!data.compositionFlow && ( +
+ Change pipeline {data.type}.
}> +
+ +
+ + + )} +
= ({ data }) => { style={{ cursor: "auto", minWidth: 110 }} > - - - + +
- - - - - +
+
+ + + - - {data.label} - - -
-
+ {data.label} + + +
{data.type === "destination" && ( diff --git a/debezium-platform-stage/src/components/pipelineDesigner/DataSelectorNode.tsx b/debezium-platform-stage/src/components/pipelineDesigner/DataSelectorNode.tsx index 9c608e7..cc585c4 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/DataSelectorNode.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/DataSelectorNode.tsx @@ -1,10 +1,4 @@ -import { - Card, - CardBody, - Bullseye, - Stack, - StackItem, -} from "@patternfly/react-core"; +import { Card, CardBody, Stack, StackItem } from "@patternfly/react-core"; import { Handle, Position } from "reactflow"; import "./DataNode.css"; import { DataSinkIcon, DataSourceIcon } from "@patternfly/react-icons"; @@ -54,30 +48,69 @@ const DataSelectorNode: React.FC = ({ data }) => { style={{ cursor: "auto", width: 110 }} > - - + + +
+ {data.type === AppStrings.source ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ {data.welcomeFlow ? ( + {data.label} + ) : ( -
- {data.type === AppStrings.source ? ( - - ) : ( - - )} -
+ {data.action}
- {data.welcomeFlow ? ({data.type}): ({data.action})} - -
-
+ )} +
{data.type === AppStrings.destination && ( diff --git a/debezium-platform-stage/src/components/pipelineDesigner/DebeziumNode.tsx b/debezium-platform-stage/src/components/pipelineDesigner/DebeziumNode.tsx index ddac885..2fbaf3d 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/DebeziumNode.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/DebeziumNode.tsx @@ -4,7 +4,6 @@ import "./DebeziumNode.css"; import dbz from "../../assets/debeziumNoText.png"; import { useData } from "../../appLayout/AppContext"; import { AppColors } from "@utils/constants"; -// import { Button } from "@patternfly/react-core"; interface DebeziumNodeProps { data: { diff --git a/debezium-platform-stage/src/components/pipelineDesigner/DestinationPipelineModel.tsx b/debezium-platform-stage/src/components/pipelineDesigner/PipelineDestinationModel.tsx similarity index 92% rename from debezium-platform-stage/src/components/pipelineDesigner/DestinationPipelineModel.tsx rename to debezium-platform-stage/src/components/pipelineDesigner/PipelineDestinationModel.tsx index b3e49c4..5d5c32f 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/DestinationPipelineModel.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/PipelineDestinationModel.tsx @@ -10,18 +10,17 @@ import { } from "@patternfly/react-core"; import React, { useCallback, useEffect, useState } from "react"; import { Destination, fetchData } from "../../apis/apis"; -// import { CreateDestinationForm } from "../../pages/Destination/CreateDestinationForm"; import { useQuery } from "react-query"; import { API_URL } from "../../utils/constants"; import SourceDestinationSelectionList from "../SourceDestinationSelectionList"; import { CatalogGrid } from "@components/CatalogGrid"; import { CreateDestination } from "@destinationPage/CreateDestination"; -type DestinationPipelineModelProps = { +type PipelineDestinationModelProps = { onDestinationSelection: (destination: Destination) => void; }; -const DestinationPipelineModel: React.FC = ({ +const PipelineDestinationModel: React.FC = ({ onDestinationSelection, }) => { const id1 = "pipeline-destination-select"; @@ -107,7 +106,6 @@ const DestinationPipelineModel: React.FC = ({ {isCreateChecked === id2 && (selectedDestination === "" ? ( - // Select the type of destination you want to connect from the list @@ -116,8 +114,6 @@ const DestinationPipelineModel: React.FC = ({ ) : ( - // - // Fill out the below form or use the smart editor to setup a new @@ -126,7 +122,6 @@ const DestinationPipelineModel: React.FC = ({ smart editor. - // ))} {isCreateChecked === id1 ? ( @@ -153,4 +148,4 @@ const DestinationPipelineModel: React.FC = ({ ); }; -export default DestinationPipelineModel; +export default PipelineDestinationModel; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/SourcePipelineModel.tsx b/debezium-platform-stage/src/components/pipelineDesigner/PipelineSourceModel.tsx similarity index 97% rename from debezium-platform-stage/src/components/pipelineDesigner/SourcePipelineModel.tsx rename to debezium-platform-stage/src/components/pipelineDesigner/PipelineSourceModel.tsx index ad702c5..f936d4b 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/SourcePipelineModel.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/PipelineSourceModel.tsx @@ -16,11 +16,11 @@ import SourceDestinationSelectionList from "../SourceDestinationSelectionList"; import { CatalogGrid } from "@components/CatalogGrid"; import { CreateSource } from "@sourcePage/CreateSource"; -type SourcePipelineModelProps = { +type PipelineSourceModelProps = { onSourceSelection: (source: Source) => void; }; -const SourcePipelineModel: React.FC = ({ +const PipelineSourceModel: React.FC = ({ onSourceSelection, }) => { const id1 = "pipeline-source-select"; @@ -147,4 +147,4 @@ const SourcePipelineModel: React.FC = ({ ); }; -export default SourcePipelineModel; +export default PipelineSourceModel; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/TransformPipelineModel.tsx b/debezium-platform-stage/src/components/pipelineDesigner/PipelineTransformModel.tsx similarity index 96% rename from debezium-platform-stage/src/components/pipelineDesigner/TransformPipelineModel.tsx rename to debezium-platform-stage/src/components/pipelineDesigner/PipelineTransformModel.tsx index 20e33ee..0ee71ac 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/TransformPipelineModel.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/PipelineTransformModel.tsx @@ -15,11 +15,11 @@ import { useQuery } from "react-query"; import TransformSelectionList from "@components/TransformSelectionList"; import { CreateTransforms } from "src/pages/Transforms"; -type TransformPipelineModelProps = { +type PipelineTransformModelProps = { onTransformSelection: (source: TransformData) => void; }; -const TransformPipelineModel: React.FC = ({ +const PipelineTransformModel: React.FC = ({ onTransformSelection, }) => { const id1 = "pipeline-transform-select"; @@ -118,4 +118,4 @@ const TransformPipelineModel: React.FC = ({ ); }; -export default TransformPipelineModel; +export default PipelineTransformModel; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/TransformAdditionNode.tsx b/debezium-platform-stage/src/components/pipelineDesigner/TransformAdditionNode.tsx index e476ae0..f2e6d67 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/TransformAdditionNode.tsx +++ b/debezium-platform-stage/src/components/pipelineDesigner/TransformAdditionNode.tsx @@ -1,6 +1,4 @@ -import { - Button, -} from "@patternfly/react-core"; +import { Button } from "@patternfly/react-core"; import { Handle, Position } from "reactflow"; import "./TransformAdditionNode.css"; @@ -37,50 +35,17 @@ const TransformAdditionNode: React.FC = ({ } > - {/* - - -
- -
-
-
- - {data.action ? ( - data.action - ) : ( - - )} - -
*/} - {data.action ? ( - data.action - ) : ( -
); }; diff --git a/debezium-platform-stage/src/components/pipelineDesigner/index.ts b/debezium-platform-stage/src/components/pipelineDesigner/index.ts index 8a10e6b..18c97aa 100644 --- a/debezium-platform-stage/src/components/pipelineDesigner/index.ts +++ b/debezium-platform-stage/src/components/pipelineDesigner/index.ts @@ -1,9 +1,8 @@ export * from './DataNode' export * from './TransformAdditionNode' export * from './CompositionFlow' -export * from './CreationFlow' export * from './CustomEdgeDestination' export * from './CustomEdgeSource' export * from './DataSelectorNode' -export * from './DestinationPipelineModel' -export * from './SourcePipelineModel' \ No newline at end of file +export * from './PipelineDestinationModel' +export * from './PipelineSourceModel' \ No newline at end of file diff --git a/debezium-platform-stage/src/pages/Pipeline/ConfigurePipeline.tsx b/debezium-platform-stage/src/pages/Pipeline/ConfigurePipeline.tsx index 96b75ce..f1a7495 100644 --- a/debezium-platform-stage/src/pages/Pipeline/ConfigurePipeline.tsx +++ b/debezium-platform-stage/src/pages/Pipeline/ConfigurePipeline.tsx @@ -142,7 +142,7 @@ const ConfigurePipeline: React.FunctionComponent = () => { const handleCreatePipeline = async (values: Record) => { console.log("values", values); setIsLoading(true); - if(!values["log-level"]) { + if (!values["log-level"]) { setLogLevelError(true); setIsLoading(false); return; @@ -356,27 +356,25 @@ const ConfigurePipeline: React.FunctionComponent = () => { fieldId="log-level-field" > { - setValue("log-level", value); - setLogLevelError(false); - }} - aria-label="FormSelect Input" - ouiaId="BasicFormSelect" - validated={ - logLevelError ? "error" : "default" - } + value={getValue("log-level")} + isRequired + id={"log-level"} + onChange={(_event, value) => { + setValue("log-level", value); + setLogLevelError(false); + }} + aria-label="FormSelect Input" + ouiaId="BasicFormSelect" + validated={logLevelError ? "error" : "default"} > - {options.map((option, index) => ( - - ))} + {options.map((option, index) => ( + + ))} diff --git a/debezium-platform-stage/src/pages/Pipeline/EditPipeline.tsx b/debezium-platform-stage/src/pages/Pipeline/EditPipeline.tsx index 308774a..ececb47 100644 --- a/debezium-platform-stage/src/pages/Pipeline/EditPipeline.tsx +++ b/debezium-platform-stage/src/pages/Pipeline/EditPipeline.tsx @@ -418,7 +418,6 @@ const EditPipeline: React.FunctionComponent = () => { isMinimapVisible language={Language.yaml} height="450px" - // className="custom-card-body" /> )} @@ -426,7 +425,6 @@ const EditPipeline: React.FunctionComponent = () => { @@ -409,12 +413,10 @@ const Pipelines: React.FunctionComponent = () => { {}} - // isReversed isDisabled /> diff --git a/debezium-platform-stage/src/pages/Transforms/CreateTransforms.tsx b/debezium-platform-stage/src/pages/Transforms/CreateTransforms.tsx index 8e284b0..c48dd8b 100644 --- a/debezium-platform-stage/src/pages/Transforms/CreateTransforms.tsx +++ b/debezium-platform-stage/src/pages/Transforms/CreateTransforms.tsx @@ -7,6 +7,7 @@ import { ButtonType, Card, CardBody, + Checkbox, Form, FormContextProvider, FormFieldGroup, @@ -16,6 +17,7 @@ import { FormHelperText, FormSelect, FormSelectOption, + FormSelectOptionProps, HelperText, HelperTextItem, MenuToggle, @@ -45,6 +47,7 @@ import { } from "@patternfly/react-icons"; import * as React from "react"; import transforms from "../../__mocks__/data/DebeziumTransfroms.json"; +import predicates from "../../__mocks__/data/Predicates.json"; import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { createPost, TransformData } from "src/apis"; @@ -97,6 +100,32 @@ const CreateTransforms: React.FunctionComponent = ({ setSelectOptions(selectOption); }, []); + const [selectedPredicate, setSelectedPredicate] = React.useState(""); + const [selectPredicateOptions, setSelectPredicateOptions] = + React.useState(); + + useEffect(() => { + const selectOption: FormSelectOptionProps[] = predicates.map((item) => { + return { + value: item.predicate, + label: item.predicate, + }; + }); + selectOption.unshift({ + value: "", + label: "Select a predicate type", + isPlaceholder: true, + }); + setSelectPredicateOptions(selectOption); + }, []); + + const onChange = ( + _event: React.FormEvent, + value: string + ) => { + setSelectedPredicate(value); + }; + useEffect(() => { const selectOption: SelectOptionProps[] = transforms.map((item) => { return { @@ -290,7 +319,7 @@ const CreateTransforms: React.FunctionComponent = ({ id="typeahead-select-input" autoComplete="off" innerRef={textInputRef} - placeholder="Select a state" + placeholder="Select a transform" {...(activeItemId && { "aria-activedescendant": activeItemId })} role="combobox" isExpanded={isOpen} @@ -316,12 +345,36 @@ const CreateTransforms: React.FunctionComponent = ({ description: description, ...restValues } = values; + const predicateConfig: Record = {}; + let transformConfig: Record = {}; + + if (selectedPredicate) { + Object.entries(restValues).forEach(([key, value]) => { + if (key.startsWith("predicate")) { + if (key.startsWith("predicateConfig.")) { + predicateConfig[key.replace("predicateConfig.", "")] = value; + } + } else { + transformConfig[key] = value; + } + }); + } else { + transformConfig = restValues; + } + const payload = { description: description, type: selected, schema: "schema321", vaults: [], - config: { ...restValues }, + config: { ...transformConfig }, + ...(selectedPredicate && { + predicate: { + type: selectedPredicate, + config: { ...predicateConfig }, + negate: values["predicate.negate"] === "true", + }, + }), name: transformName, }; const response = await createPost(`${API_URL}/api/transforms`, payload); @@ -346,7 +399,7 @@ const CreateTransforms: React.FunctionComponent = ({ find(transforms, { transform: selected })?.properties || {} ).reduce((acc: Record, [key, value]) => { if (value.defaultValue) { - acc[`debezium.transforms.transform-name.${key}`] = value.defaultValue; + acc[key] = value.defaultValue; } return acc; }, {}); @@ -565,7 +618,7 @@ const CreateTransforms: React.FunctionComponent = ({ @@ -574,44 +627,28 @@ const CreateTransforms: React.FunctionComponent = ({ > {isBoolean ? ( { const value = checked ? "true" : "false"; - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} /> ) : dropDown ? ( { - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} aria-label="FormSelect Input" ouiaId="BasicFormSelect" @@ -631,30 +668,18 @@ const CreateTransforms: React.FunctionComponent = ({ ) : ( { - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} value={ - getValue( - `debezium.transforms.${transformName}.${key}` - ) || property?.defaultValue + getValue(key) || property?.defaultValue } validated={ - errors[ - `debezium.transforms.${transformName}.${key}` - ] - ? "error" - : "default" + errors[key] ? "error" : "default" } /> )} @@ -663,23 +688,13 @@ const CreateTransforms: React.FunctionComponent = ({ , })} > - { - errors[ - `debezium.transforms.${transformName}.${key}` - ] - } + {errors[key]} @@ -688,6 +703,113 @@ const CreateTransforms: React.FunctionComponent = ({ })} )} + + + } + > + + + + } + fieldId="predicate-type" + > + + {selectPredicateOptions?.map((option, index) => ( + + ))} + + + {selectedPredicate && + selectedPredicate !== + "org.apache.kafka.connect.transforms.predicates.RecordIsTombstone" && + (() => { + const predicate = predicates.find( + (predicate) => + predicate.predicate === selectedPredicate + ); + const predicateProperties = + predicate?.properties as Record; + const propertyKey = Object.keys( + predicateProperties || {} + )[0]; + const property = predicateProperties?.[propertyKey]; + + return ( + + + + } + fieldId="predicate-config" + > + { + setValue( + `predicateConfig.${property["x-name"]}`, + value + ); + // setError("description", undefined); + }} + value={getValue( + `predicateConfig.${property["x-name"]}` + )} + /> + + ); + })()} + {selectedPredicate && ( + , + checked: boolean + ) => { + setValue("predicate.negate", "" + checked); + setError("predicate.negate", undefined); + }} + description="Setting this option to True applies the predicate only to records that do not match the defined predicate condition" + /> + )} + diff --git a/debezium-platform-stage/src/pages/Transforms/EditTransforms.tsx b/debezium-platform-stage/src/pages/Transforms/EditTransforms.tsx index bdd2855..c1d09c5 100644 --- a/debezium-platform-stage/src/pages/Transforms/EditTransforms.tsx +++ b/debezium-platform-stage/src/pages/Transforms/EditTransforms.tsx @@ -7,6 +7,7 @@ import { ButtonType, Card, CardBody, + Checkbox, Form, FormContextProvider, FormFieldGroup, @@ -16,6 +17,7 @@ import { FormHelperText, FormSelect, FormSelectOption, + FormSelectOptionProps, HelperText, HelperTextItem, MenuToggle, @@ -44,6 +46,7 @@ import { } from "@patternfly/react-icons"; import * as React from "react"; import transforms from "../../__mocks__/data/DebeziumTransfroms.json"; +import predicates from "../../__mocks__/data/Predicates.json"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { editPut, fetchDataTypeTwo, TransformData } from "src/apis"; @@ -99,6 +102,32 @@ const EditTransforms: React.FunctionComponent = ({ setSelectOptions(selectOption); }, []); + const [selectedPredicate, setSelectedPredicate] = React.useState(""); + const [selectPredicateOptions, setSelectPredicateOptions] = + React.useState(); + + useEffect(() => { + const selectOption: FormSelectOptionProps[] = predicates.map((item) => { + return { + value: item.predicate, + label: item.predicate, + }; + }); + selectOption.unshift({ + value: "", + label: "Select a predicate type", + isPlaceholder: true, + }); + setSelectPredicateOptions(selectOption); + }, []); + + const onChange = ( + _event: React.FormEvent, + value: string + ) => { + setSelectedPredicate(value); + }; + useEffect(() => { const selectOption: SelectOptionProps[] = transforms.map((item) => { return { @@ -285,7 +314,7 @@ const EditTransforms: React.FunctionComponent = ({ id="typeahead-select-input" autoComplete="off" innerRef={textInputRef} - placeholder="Select a state" + placeholder="Select a transform" {...(activeItemId && { "aria-activedescendant": activeItemId })} role="combobox" isExpanded={isOpen} @@ -313,12 +342,22 @@ const EditTransforms: React.FunctionComponent = ({ setError(response.error); } else { setSelected(response.data?.type || ""); + setSelectedPredicate(response.data?.predicate?.type || ""); setInputValue(response.data?.type || ""); setTransformData(response.data as TransformData); setInitialValues({ "transform-name": response.data?.name || "", description: response.data?.description || "", ...response.data?.config, + ...Object.keys(response.data?.predicate?.config || {}).reduce( + (acc: Record, key) => { + acc[`predicateConfig.${key}`] = + response.data?.predicate?.config[key]; + return acc; + }, + {} as Record + ), + "predicate.negate": "" + response.data?.predicate?.negate || "false", }); } @@ -336,41 +375,42 @@ const EditTransforms: React.FunctionComponent = ({ ...restValues } = values; const configProperties = transformData?.config || {}; - const oldConfig = Object.keys(configProperties).reduce( - (acc: Record, key) => { - const newKey = key.replace( - /(?<=debezium\.transforms\.)[^.]+/, - transformName - ); - acc[newKey] = configProperties![key]; - return acc; - }, - {} - ); - const oldTransName = initialValues["transform-name"]; + const oldConfig = { ...configProperties }; + const oldPredicates = transformData?.predicate || {}; + + const newValues: Record = {}; + const newPredicates: any = { config: {} }; - const newValues = Object.keys(restValues).reduce>( - (acc, key) => { - if (key.includes(oldTransName)) { - const newKey = key.replace( - /(?<=debezium\.transforms\.)[^.]+/, - transformName - ); - acc[newKey] = restValues[key]; + Object.entries(restValues).forEach(([key, value]) => { + if (key.startsWith("predicate")) { + if (key.startsWith("predicateConfig.")) { + newPredicates.config[key.replace("predicateConfig.", "")] = value; } else { - acc[key] = restValues[key]; + newPredicates[key.replace("predicate.", "")] = value; } - return acc; - }, - {} - ); + } else { + newValues[key] = value; + } + }); + if (selectedPredicate) { + newPredicates.type = selectedPredicate; + } const updatedConfig = { ...oldConfig, ...newValues }; + const updatedPredicates = + Object.keys(newPredicates).length > 1 || !isEmpty(newPredicates.config) + ? { + ...oldPredicates, + ...newPredicates, + config: { ...newPredicates.config }, + } + : {}; const payload = { description: description, config: { ...updatedConfig }, + predicate: { ...updatedPredicates }, name: transformName, }; @@ -394,7 +434,14 @@ const EditTransforms: React.FunctionComponent = ({ navigateTo("/transform"); } }, - [addNotification, transformData, initialValues, onSelection, selected] + [ + addNotification, + transformData, + initialValues, + onSelection, + selectedPredicate, + selected, + ] ); const handleItemClick = ( @@ -614,7 +661,7 @@ const EditTransforms: React.FunctionComponent = ({ @@ -623,46 +670,30 @@ const EditTransforms: React.FunctionComponent = ({ > {isBoolean ? ( { const value = checked ? "true" : "false"; - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} /> ) : dropDown ? ( { - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} aria-label="FormSelect Input" ouiaId="BasicFormSelect" @@ -682,32 +713,20 @@ const EditTransforms: React.FunctionComponent = ({ ) : ( { - setValue( - `debezium.transforms.${transformName}.${key}`, - value - ); - setError( - `debezium.transforms.${transformName}.${key}`, - undefined - ); + setValue(key, value); + setError(key, undefined); }} defaultValue={ (!isEmpty(initialValues) && - getValue( - `debezium.transforms.${transformName}.${key}` - )) || + getValue(key)) || property?.defaultValue } validated={ - errors[ - `debezium.transforms.${transformName}.${key}` - ] - ? "error" - : "default" + errors[key] ? "error" : "default" } /> )} @@ -716,23 +735,13 @@ const EditTransforms: React.FunctionComponent = ({ , })} > - { - errors[ - `debezium.transforms.${transformName}.${key}` - ] - } + {errors[key]} @@ -741,6 +750,120 @@ const EditTransforms: React.FunctionComponent = ({ })} )} + + } + > + + + + } + fieldId="predicate-type" + > + + {selectPredicateOptions?.map((option, index) => ( + + ))} + + + {selectedPredicate && + selectedPredicate !== + "org.apache.kafka.connect.transforms.predicates.RecordIsTombstone" && + (() => { + const predicate = predicates.find( + (predicate) => + predicate.predicate === selectedPredicate + ); + const predicateProperties = + predicate?.properties as Record; + const propertyKey = Object.keys( + predicateProperties || {} + )[0]; + const property = + predicateProperties?.[propertyKey]; + + return ( + + + + } + fieldId="predicate-config" + > + { + setValue( + `predicateConfig.${property["x-name"]}`, + value + ); + // setError("description", undefined); + }} + value={getValue( + `predicateConfig.${property["x-name"]}` + )} + /> + + ); + })()} + {selectedPredicate && ( + + , + checked: boolean + ) => { + setValue("predicate.negate", "" + checked); + setError("predicate.negate", undefined); + }} + description="Setting this option to True applies the predicate only to records that do not match the defined predicate condition" + /> + + )} +