This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit 454e0a88006430202a95746267f9743dc0a410f2 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Wed Feb 14 10:50:02 2024 -0500 Incoming navigation for #1109 --- karavan-designer/public/example/demo.camel.yaml | 8 +- karavan-designer/src/App.tsx | 108 +++++++++------------ karavan-designer/src/DesignerPage.tsx | 11 +-- karavan-designer/src/designer/KaravanDesigner.tsx | 2 +- .../src/designer/route/DslConnections.tsx | 65 ++++++++++--- .../src/designer/utils/InfrastructureAPI.ts | 4 +- karavan-designer/src/topology/TopologyStore.ts | 14 +++ karavan-designer/src/topology/TopologyTab.tsx | 11 +-- 8 files changed, 131 insertions(+), 92 deletions(-) diff --git a/karavan-designer/public/example/demo.camel.yaml b/karavan-designer/public/example/demo.camel.yaml index 9d74d5d4..28340ccf 100644 --- a/karavan-designer/public/example/demo.camel.yaml +++ b/karavan-designer/public/example/demo.camel.yaml @@ -44,7 +44,7 @@ id: to-2df4 uri: direct parameters: - name: first-firect + name: second_direct - to: id: to-e017 uri: direct @@ -57,6 +57,12 @@ uri: direct parameters: name: first-firect + steps: + - to: + id: to-5c86 + uri: direct + parameters: + name: second_direct - route: id: second_direct from: diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx index a6613131..9217fa82 100644 --- a/karavan-designer/src/App.tsx +++ b/karavan-designer/src/App.tsx @@ -41,7 +41,9 @@ import {KnowledgebasePage} from "./knowledgebase/KnowledgebasePage"; import {Notification} from "./designer/utils/Notification"; import {EventBus} from "./designer/utils/EventBus"; import {TopologyTab} from "./topology/TopologyTab"; -import {IntegrationFile} from "./topology/TopologyStore"; +import {useEffect, useState} from "react"; +import {IntegrationFile, useTopologyStore} from "./topology/TopologyStore"; +import {shallow} from "zustand/shallow"; class MenuItem { pageId: string = ''; @@ -55,27 +57,16 @@ class MenuItem { } } -interface Props { -} - -interface State { - name: string - yaml: string - key: string - loaded?: boolean, - pageId: string, -} +export function App() { -class App extends React.Component<Props, State> { + const [setFiles] = useTopologyStore((s) => [s.setFiles], shallow); + const [pageId, setPageId] = useState<string>('designer'); + const [name, setName] = useState<string>('example.yaml'); + const [key, setKey] = useState<string>(''); + const [yaml, setYaml] = useState<string>(''); + const [loaded, setLoaded] = useState<boolean>(false); - public state: State = { - pageId: "designer", - name: 'example.yaml', - key: '', - yaml: '' - } - - componentDidMount() { + useEffect(() => { Promise.all([ fetch("kamelets/kamelets.yaml"), fetch("components/components.json"), @@ -96,14 +87,15 @@ class App extends React.Component<Props, State> { JSON.parse(data[1]).forEach((c: any) => jsons.push(JSON.stringify(c))); ComponentApi.saveComponents(jsons, true); - this.setState({loaded: true}); + setLoaded(true); TemplateApi.saveTemplate("org.apache.camel.AggregationStrategy", data[2]); TemplateApi.saveTemplate("org.apache.camel.Processor", data[3]); if (data[4]) { - this.setState({yaml: data[4], name: "demo.camel.yaml"}) - // this.setState({yaml: data[4], name: "aws-s3-cdc-source.kamelet.yaml"}) + setYaml(data[4]); + setName("demo.camel.yaml"); + setFiles([new IntegrationFile("demo.camel.yaml", yaml)]); } if (data[5]) { @@ -113,13 +105,13 @@ class App extends React.Component<Props, State> { }).catch(err => EventBus.sendAlert("Error", err.text, 'danger') ); - } + }); - save(filename: string, yaml: string, propertyOnly: boolean) { - console.log(yaml); + function save(filename: string, yaml: string, propertyOnly: boolean) { + // console.log(yaml); } - getSpinner() { + function getSpinner() { return ( <Bullseye className="loading-page"> <Spinner className="progress-stepper" diameter="80px" aria-label="Loading..."/> @@ -127,8 +119,7 @@ class App extends React.Component<Props, State> { ) } - pageNav = () => { - const {pageId} = this.state; + function pageNav () { const pages: MenuItem[] = [ new MenuItem("designer", "Designer", <BlueprintIcon/>), new MenuItem("topology", "Topology", <TopologyIcon/>), @@ -147,7 +138,7 @@ class App extends React.Component<Props, State> { <Tooltip content={page.tooltip} position={"right"}> <Button id={page.pageId} icon={page.icon} variant={"plain"} className={pageId === page.pageId ? "nav-button-selected" : ""} - onClick={event => this.setState({pageId: page.pageId})} + onClick={event => setPageId(page.pageId)} /> </Tooltip> </FlexItem> @@ -158,12 +149,7 @@ class App extends React.Component<Props, State> { </Flex>) } - getIntegrationFiles(): IntegrationFile[]{ - return [new IntegrationFile("demo.camel.yaml", this.state.yaml)]; - } - - getPage() { - const { name, yaml, pageId} = this.state; + function getPage() { const dark = document.body.className.includes('vscode-dark'); switch (pageId) { case "designer": @@ -171,7 +157,7 @@ class App extends React.Component<Props, State> { <DesignerPage name={name} yaml={yaml} - onSave={(filename, yaml1, propertyOnly) => this.save(filename, yaml1, propertyOnly)} + onSave={(filename, yaml1, propertyOnly) => save(filename, yaml1, propertyOnly)} dark={dark}/> ) case "knowledgebase": @@ -181,7 +167,6 @@ class App extends React.Component<Props, State> { case "topology": return ( <TopologyTab - files={this.getIntegrationFiles()} onSetFile={fileName => {}} onClickAddRoute={() => {}} onClickAddREST={() => {}} @@ -192,35 +177,32 @@ class App extends React.Component<Props, State> { } } - getHeader = () => ( - <Masthead> - </Masthead> - ); + function getHeader () { + return (<Masthead> + </Masthead>) + } - getSidebar = () => ( - <PageSidebar isSidebarOpen={true} id="fill-sidebar"> + function getSidebar () { + return (<PageSidebar isSidebarOpen={true} id="fill-sidebar"> <PageSidebarBody>Navigation</PageSidebarBody> - </PageSidebar> - ); + </PageSidebar>) + } - public render() { - const {loaded} = this.state; - return ( - <Page className="karavan"> - <Notification/> - <Flex direction={{default: "row"}} style={{width: "100%", height: "100%"}} - alignItems={{default: "alignItemsStretch"}} spaceItems={{default: 'spaceItemsNone'}}> - <FlexItem> - {this.pageNav()} - </FlexItem> - <FlexItem flex={{default: "flex_2"}} style={{height: "100%"}}> - {loaded !== true && this.getSpinner()} - {loaded === true && this.getPage()} + return ( + <Page className="karavan"> + <Notification/> + <Flex direction={{default: "row"}} style={{width: "100%", height: "100%"}} + alignItems={{default: "alignItemsStretch"}} spaceItems={{default: 'spaceItemsNone'}}> + <FlexItem> + {pageNav()} </FlexItem> - </Flex> - </Page> - ) - } + <FlexItem flex={{default: "flex_2"}} style={{height: "100%"}}> + {!loaded && getSpinner()} + {loaded && getPage()} + </FlexItem> + </Flex> + </Page> + ) } export default App; diff --git a/karavan-designer/src/DesignerPage.tsx b/karavan-designer/src/DesignerPage.tsx index 56bde9e3..4a30e899 100644 --- a/karavan-designer/src/DesignerPage.tsx +++ b/karavan-designer/src/DesignerPage.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import { Toolbar, ToolbarContent, @@ -38,11 +38,6 @@ export const DesignerPage = (props: Props) => { const [yaml, setYaml] = useState<string>(props.yaml); - useEffect(() => { - console.log("DesignerPage") - // setYaml(); - }, []); - function save(filename: string, yaml: string, propertyOnly: boolean) { setYaml(yaml); props.onSave(filename, yaml, propertyOnly); @@ -82,8 +77,8 @@ export const DesignerPage = (props: Props) => { ]} onSavePropertyPlaceholder={(key, value) => console.log("onSavePropertyPlaceholder", key, value)} beans={[]} - onInternalConsumerClick={(uri, name) => { - console.log("onInternalConsumerClick", uri, name) + onInternalConsumerClick={(uri, name, direction: 'from' | 'to') => { + console.log("onInternalConsumerClick", uri, name, direction) }} /> ) diff --git a/karavan-designer/src/designer/KaravanDesigner.tsx b/karavan-designer/src/designer/KaravanDesigner.tsx index 05831a38..75e6c012 100644 --- a/karavan-designer/src/designer/KaravanDesigner.tsx +++ b/karavan-designer/src/designer/KaravanDesigner.tsx @@ -48,7 +48,7 @@ interface Props { onSaveCustomCode: (name: string, code: string) => void onGetCustomCode: (name: string, javaType: string) => Promise<string | undefined> onSavePropertyPlaceholder: (key: string, value: string) => void - onInternalConsumerClick: (uri: string, name: string) => void + onInternalConsumerClick: (uri: string, name: string, direction: 'from' | 'to') => void filename: string yaml: string dark: boolean diff --git a/karavan-designer/src/designer/route/DslConnections.tsx b/karavan-designer/src/designer/route/DslConnections.tsx index ebb89933..cab339f8 100644 --- a/karavan-designer/src/designer/route/DslConnections.tsx +++ b/karavan-designer/src/designer/route/DslConnections.tsx @@ -24,28 +24,46 @@ import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt" import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils"; import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; import {v4 as uuidv4} from "uuid"; -import {DeleteElementIcon} from "../utils/ElementIcons"; -import {Button} from "@patternfly/react-core"; +import {Button, Tooltip} from "@patternfly/react-core"; import {InfrastructureAPI} from "../utils/InfrastructureAPI"; +import {useTopologyStore} from "../../topology/TopologyStore"; +import {getIntegrations} from "../../topology/TopologyApi"; const overlapGap: number = 40; export function DslConnections() { const [integration] = useIntegrationStore((state) => [state.integration], shallow) + const [files] = useTopologyStore((s) => [s.files], shallow); const [width, height, top, left, hideLogDSL] = useDesignerStore((s) => [s.width, s.height, s.top, s.left, s.hideLogDSL], shallow) const [steps, addStep, deleteStep, clearSteps] = useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps], shallow) const [svgKey, setSvgKey] = useState<string>('svgKey'); + const [tons, setTons] = useState<Map<string, string[]>>(new Map<string, string[]>()); useEffect(() => { + const integrations = getIntegrations(files); + setTons(prevState => { + const data = new Map<string, string[]>(); + TopologyUtils.findTopologyOutgoingNodes(integrations).forEach(t => { + const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters.name; + if (data.has(key)) { + const list = data.get(key) || []; + list.push(t.routeId); + data.set(key, list); + } else { + data.set(key, [t.routeId]); + } + }); + return data; + }); const sub1 = EventBus.onPosition()?.subscribe((evt: DslPosition) => setPosition(evt)); return () => { sub1?.unsubscribe(); }; - }); + }, [files]); useEffect(() => { const toDelete1: string[] = Array.from(steps.keys()).filter(k => CamelDefinitionApiExt.findElementInIntegration(integration, k) === undefined); @@ -67,7 +85,7 @@ export function DslConnections() { let outs: [string, number][] = Array.from(steps.values()) .filter(pos => ["FromDefinition"].includes(pos.step.dslName)) .filter(pos => !TopologyUtils.isElementInternalComponent(pos.step)) - .filter(pos => !(pos.step.dslName === 'FromDefinition' && TopologyUtils.hasInternalUri(pos.step))) + // .filter(pos => !(pos.step.dslName === 'FromDefinition' && TopologyUtils.hasInternalUri(pos.step))) .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source')) .sort((pos1: DslPosition, pos2: DslPosition) => { const y1 = pos1.headerRect.y + pos1.headerRect.height / 2; @@ -104,9 +122,22 @@ export function DslConnections() { } } + function getToDirectSteps(name: string) { + return Array.from(steps.values()) + .filter(s => s.step.dslName === 'ToDefinition') + .filter(s => ['direct','seda'].includes((s.step as any)?.uri)) + .filter(s => (s.step as any)?.parameters?.name === name) + } + function getIncomingIcons(data: [string, number]) { const pos = steps.get(data[0]); if (pos) { + const step = (pos.step as any); + const uri = step?.uri; + const directOrSeda: boolean = step && uri && step?.dslName === 'FromDefinition' && ['direct','seda'].includes(uri); + const name: string = directOrSeda ? (step?.parameters?.name) : undefined; + const routes = directOrSeda ? tons.get(uri + ':' +name) || [] : []; + const localDirects = getToDirectSteps(name); const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; const r = pos.headerRect.height / 2; const incomingX = 20; @@ -116,6 +147,16 @@ export function DslConnections() { <div key={pos.step.uuid + "-icon"} style={{display: "block", position: "absolute", top: imageY, left: imageX}}> {CamelUi.getConnectionIcon(pos.step)} + {routes.map((routeId, index) => + <Tooltip key={`${routeId}:${index}`} content={`Go to route:${routeId}`} position={"right"}> + <Button style={{position: 'absolute', left: 27, top: (index * 16) + (12), whiteSpace: 'nowrap', zIndex: 300, padding: 0}} + variant={'link'} + aria-label="Goto" + onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name, 'from')}> + {routeId} + </Button> + </Tooltip> + )} </div> ) } @@ -204,13 +245,15 @@ export function DslConnections() { <div key={pos.step.uuid + "-icon"} style={{display: "block", position: "absolute", top: imageY, left: imageX}}> {CamelUi.getConnectionIcon(pos.step)} - {directOrSeda && - <Button style={{position: 'absolute', right: 27, top: -12, whiteSpace: 'nowrap', zIndex: 300, padding: 0}} - variant={'link'} - aria-label="Goto" - onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name)}> - {name} - </Button> + {name !== undefined && + <Tooltip content={`Go to ${uri}:${name}`} position={"left"}> + <Button style={{position: 'absolute', right: 27, top: -12, whiteSpace: 'nowrap', zIndex: 300, padding: 0}} + variant={'link'} + aria-label="Goto" + onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name, 'to')}> + {name} + </Button> + </Tooltip> } </div> ) diff --git a/karavan-designer/src/designer/utils/InfrastructureAPI.ts b/karavan-designer/src/designer/utils/InfrastructureAPI.ts index 77ef1bd3..2acd5d54 100644 --- a/karavan-designer/src/designer/utils/InfrastructureAPI.ts +++ b/karavan-designer/src/designer/utils/InfrastructureAPI.ts @@ -21,7 +21,7 @@ export class InfrastructureAPI { static onSaveCustomCode: (name: string, code: string) => void; static onSave: (filename: string, yaml: string, propertyOnly: boolean) => void; static onSavePropertyPlaceholder: (key: string, value: string) => void; - static onInternalConsumerClick: (uri: string, name: string) => void; + static onInternalConsumerClick: (uri: string, name: string, direction: 'from' | 'to') => void; static setOnGetCustomCode(onGetCustomCode: (name: string, javaType: string) => Promise<string | undefined>){ this.onGetCustomCode = onGetCustomCode @@ -39,7 +39,7 @@ export class InfrastructureAPI { this.onSavePropertyPlaceholder = onSavePropertyPlaceholder } - static setOnInternalConsumerClick(onInternalConsumerClick:(uri: string, name: string) => void){ + static setOnInternalConsumerClick(onInternalConsumerClick:(uri: string, name: string, direction: 'from' | 'to') => void){ this.onInternalConsumerClick = onInternalConsumerClick } diff --git a/karavan-designer/src/topology/TopologyStore.ts b/karavan-designer/src/topology/TopologyStore.ts index 4e9ee643..c6322f39 100644 --- a/karavan-designer/src/topology/TopologyStore.ts +++ b/karavan-designer/src/topology/TopologyStore.ts @@ -29,6 +29,9 @@ export class IntegrationFile { } interface TopologyState { + files: IntegrationFile [] + setFiles: (files: IntegrationFile []) => void + resetFiles: (files: IntegrationFile []) => void selectedIds: string [] fileName?: string setSelectedIds: (selectedIds: string []) => void @@ -40,6 +43,17 @@ interface TopologyState { } export const useTopologyStore = createWithEqualityFn<TopologyState>((set) => ({ + files: [], + setFiles: (files: IntegrationFile []) => { + set((state: TopologyState) => { + return {files: files}; + }); + }, + resetFiles: (files: IntegrationFile []) => { + set((state: TopologyState) => { + return {files: [...files]}; + }); + }, selectedIds: [], setSelectedIds: (selectedIds: string[]) => { set((state: TopologyState) => { diff --git a/karavan-designer/src/topology/TopologyTab.tsx b/karavan-designer/src/topology/TopologyTab.tsx index 1f2d165e..bf16124a 100644 --- a/karavan-designer/src/topology/TopologyTab.tsx +++ b/karavan-designer/src/topology/TopologyTab.tsx @@ -37,7 +37,6 @@ import {TopologyToolbar} from "./TopologyToolbar"; import {useDesignerStore} from "../designer/DesignerStore"; interface Props { - files: IntegrationFile[], onSetFile: (fileName: string) => void hideToolbar: boolean onClickAddRoute: () => void @@ -47,8 +46,8 @@ interface Props { export function TopologyTab(props: Props) { - const [selectedIds, setSelectedIds, setFileName, ranker, setRanker, setNodeData] = useTopologyStore((s) => - [s.selectedIds, s.setSelectedIds, s.setFileName, s.ranker, s.setRanker, s.setNodeData], shallow); + const [selectedIds, setSelectedIds, setFileName, ranker, setRanker, setNodeData, files] = useTopologyStore((s) => + [s.selectedIds, s.setSelectedIds, s.setFileName, s.ranker, s.setRanker, s.setNodeData, s.files], shallow); const [setSelectedStep] = useDesignerStore((s) => [s.setSelectedStep], shallow) function setTopologySelected(model: Model, ids: string []) { @@ -70,7 +69,7 @@ export function TopologyTab(props: Props) { } const controller = React.useMemo(() => { - const model = getModel(props.files); + const model = getModel(files); const newController = new Visualization(); newController.registerLayoutFactory((_, graph) => new DagreLayout(graph, { @@ -97,9 +96,9 @@ export function TopologyTab(props: Props) { React.useEffect(() => { setSelectedIds([]) - const model = getModel(props.files); + const model = getModel(files); controller.fromModel(model, false); - }, [ranker, controller, setSelectedIds, props.files]); + }, [ranker, controller, setSelectedIds, files]); const controlButtons = React.useMemo(() => { // const customButtons = [