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 8a2f0fedabf7abaf5adab5e433205dcdf69f0977
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 15 18:14:36 2023 -0400

    First prototype #893
---
 karavan-core/src/core/api/CamelDisplayUtil.ts      |  58 +-
 karavan-core/src/core/api/TopologyUtils.ts         | 218 ++++++
 karavan-core/src/core/model/TopologyDefinition.ts  |  87 +++
 karavan-core/test/topology.spec.ts                 |  36 +
 karavan-core/test/topology1.camel.yaml             |  56 ++
 karavan-core/test/topology2.camel.yaml             |  44 ++
 karavan-designer/package-lock.json                 | 845 ++++++++++++++++++++-
 karavan-designer/package.json                      |   1 +
 .../src/designer/route/DslConnections.tsx          |  16 +-
 karavan-designer/src/designer/route/DslElement.tsx |   6 +-
 .../src/designer/route/DslProperties.tsx           |   5 +-
 karavan-designer/src/designer/utils/CamelUi.tsx    |  74 +-
 .../karavan-app/src/main/webui/package-lock.json   | 673 +++++++++++++++-
 .../karavan-app/src/main/webui/package.json        |   4 +
 .../main/webui/src/designer/KaravanDesigner.tsx    |   2 -
 .../webui/src/designer/route/DslConnections.tsx    |   8 +-
 .../src/main/webui/src/designer/utils/CamelUi.tsx  |  59 +-
 .../src/main/webui/src/project/ProjectPage.tsx     |   1 +
 .../src/main/webui/src/project/ProjectPanel.tsx    |  18 +-
 .../main/webui/src/project/topology/CustomNode.tsx |  42 +
 .../webui/src/project/topology/TopologyApi.tsx     | 168 ++++
 .../webui/src/project/topology/TopologyTab.tsx     |  74 ++
 .../webui/src/project/topology/TopologyToolbar.tsx |  49 ++
 .../main/webui/src/project/topology/topology.css   |   4 +
 24 files changed, 2369 insertions(+), 179 deletions(-)

diff --git a/karavan-core/src/core/api/CamelDisplayUtil.ts 
b/karavan-core/src/core/api/CamelDisplayUtil.ts
index eee33ccb..0e0c79e2 100644
--- a/karavan-core/src/core/api/CamelDisplayUtil.ts
+++ b/karavan-core/src/core/api/CamelDisplayUtil.ts
@@ -18,21 +18,37 @@ import { Integration, CamelElement } from 
'../model/IntegrationDefinition';
 import { CamelUtil } from './CamelUtil';
 import { CamelDefinitionApi } from './CamelDefinitionApi';
 import { CamelDefinitionApiExt } from './CamelDefinitionApiExt';
+import { KameletModel } from '../model/KameletModels';
+import { RouteDefinition } from '../model/CamelDefinition';
+import { ComponentApi } from './ComponentApi';
+import { CamelMetadataApi } from '../model/CamelMetadata';
 
 export class CamelDisplayUtil {
     private constructor() {}
 
-    static isStepDefinitionExpanded = (
-        integration: Integration,
-        stepUuid: string,
-        selectedUuid: string | undefined,
-    ): boolean => {
+    static getTitle = (element: CamelElement): string => {
+        const k: KameletModel | undefined = CamelUtil.getKamelet(element);
+        if (k) {
+            return k.title();
+        } else if (element.dslName === 'RouteDefinition') {
+            const routeId = (element as RouteDefinition).id
+            return routeId ? routeId : CamelUtil.capitalizeName((element as 
any).stepName);
+        } else if ((element as any).uri && (['ToDefinition', 
'FromDefinition'].includes(element.dslName))) {
+            const uri = (element as any).uri
+            return ComponentApi.getComponentTitleFromUri(uri) || '';
+        } else {
+            const title = 
CamelMetadataApi.getCamelModelMetadataByClassName(element.dslName);
+            return title ? title.title : CamelUtil.capitalizeName((element as 
any).stepName);
+        }
+    }
+
+    static isStepDefinitionExpanded = (integration: Integration, stepUuid: 
string, selectedUuid: string | undefined): boolean => {
         const expandedUuids: string[] = [];
         if (selectedUuid) {
             
expandedUuids.push(...CamelDisplayUtil.getParentStepDefinitions(integration, 
selectedUuid));
         }
         return expandedUuids.includes(stepUuid);
-    };
+    }
 
     static getParentStepDefinitions = (integration: Integration, uuid: 
string): string[] => {
         const result: string[] = [];
@@ -50,7 +66,7 @@ export class CamelDisplayUtil {
             }
         }
         return result;
-    };
+    }
 
     static setIntegrationVisibility = (integration: Integration, selectedUuid: 
string | undefined): Integration => {
         const clone: Integration = CamelUtil.cloneIntegration(integration);
@@ -73,7 +89,7 @@ export class CamelDisplayUtil {
 
         clone.spec.flows = flows;
         return clone;
-    };
+    }
 
     static setElementVisibility = (step: CamelElement, showChildren: boolean, 
expandedUuids: string[]): CamelElement => {
         const result = CamelDefinitionApi.createStep(step.dslName, step);
@@ -82,34 +98,22 @@ export class CamelDisplayUtil {
             showChildren = expandedUuids.includes(result.uuid);
         }
 
-        const elementChildDefiniton = 
CamelDefinitionApiExt.getElementChildrenDefinition(step.dslName);
-        for (const element of elementChildDefiniton) {
+        const elementChildDefinition = 
CamelDefinitionApiExt.getElementChildrenDefinition(step.dslName);
+        for (const element of elementChildDefinition) {
             const camelElement = 
CamelDefinitionApiExt.getElementChildren(step, element);
             if (element.multiple) {
-                (result as any)[element.name] = 
CamelDisplayUtil.setElementsVisibility(
-                    (result as any)[element.name],
-                    showChildren,
-                    expandedUuids,
-                );
+                (result as any)[element.name] = 
CamelDisplayUtil.setElementsVisibility((result as any)[element.name], 
showChildren, expandedUuids)
             } else {
                 const prop = (result as any)[element.name];
                 if (prop && prop.hasOwnProperty('uuid')) {
-                    (result as any)[element.name] = 
CamelDisplayUtil.setElementVisibility(
-                        camelElement[0],
-                        showChildren,
-                        expandedUuids,
-                    );
+                    (result as any)[element.name] = 
CamelDisplayUtil.setElementVisibility(camelElement[0], 
showChildren,expandedUuids)
                 }
             }
         }
         return result;
-    };
+    }
 
-    static setElementsVisibility = (
-        steps: CamelElement[] | undefined,
-        showChildren: boolean,
-        expandedUuids: string[],
-    ): CamelElement[] => {
+    static setElementsVisibility = (steps: CamelElement[] | undefined, 
showChildren: boolean, expandedUuids: string[]): CamelElement[] => {
         const result: CamelElement[] = [];
         if (steps) {
             for (const step of steps) {
@@ -117,5 +121,5 @@ export class CamelDisplayUtil {
             }
         }
         return result;
-    };
+    }
 }
diff --git a/karavan-core/src/core/api/TopologyUtils.ts 
b/karavan-core/src/core/api/TopologyUtils.ts
new file mode 100644
index 00000000..29ede6bb
--- /dev/null
+++ b/karavan-core/src/core/api/TopologyUtils.ts
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { CamelMetadataApi} from '../model/CamelMetadata';
+import {
+    DeleteDefinition,
+    FromDefinition,
+    GetDefinition,
+    HeadDefinition,
+    PatchDefinition,
+    PostDefinition,
+    PutDefinition,
+    RestDefinition, SagaDefinition,
+} from '../model/CamelDefinition';
+import {
+    CamelElement,
+    CamelElementMeta,
+    Integration,
+} from '../model/IntegrationDefinition';
+import { CamelDefinitionApi } from './CamelDefinitionApi';
+import {
+    TopologyIncomingNode,
+    TopologyOutgoingNode,
+    TopologyRestNode,
+    TopologyRouteNode,
+} from '../model/TopologyDefinition';
+import { ComponentApi } from './ComponentApi';
+import { CamelDefinitionApiExt } from './CamelDefinitionApiExt';
+import { CamelDisplayUtil } from './CamelDisplayUtil';
+
+const outgoingDefinitions: string[] = ['ToDefinition', 'KameletDefinition', 
'ToDynamicDefinition', "PollEnrichDefinition", "EnrichDefinition", 
"WireTapDefinition", "SagaDefinition"];
+
+export class ChildElement {
+    constructor(public name: string = '', public className: string = '', 
public multiple: boolean = false) {}
+}
+
+export class TopologyUtils {
+    private constructor() {}
+
+    static getOutgoingDefinitions = (): string[] => {
+        return outgoingDefinitions;
+    }
+
+    static isElementInternalComponent = (element: CamelElement): boolean => {
+        const uri = (element as any).uri;
+        const component = ComponentApi.findByName(uri);
+        return component !== undefined &&
+            (TopologyUtils.isComponentInternal(component.component.label) || 
TopologyUtils.hasInternalUri(element));
+    }
+
+    static isComponentInternal = (label: string): boolean => {
+        const labels = label.split(",");
+        if (labels.includes('core') && (
+            labels.includes('transformation')
+            || labels.includes('testing')
+            || labels.includes('scheduling')
+            || labels.includes('monitoring')
+            || labels.includes('transformation')
+            || labels.includes('java')
+            || labels.includes('endpoint')
+            || labels.includes('script')
+            || labels.includes('validation')
+        )) {
+            return true;
+        } else if (label === 'transformation') {
+            return true;
+        }
+        return false;
+    }
+
+    static hasInternalUri = (element: CamelElement): boolean => {
+        return this.hasDirectUri(element) || this.hasSedaUri(element);
+    }
+
+    static hasDirectUri = (element: CamelElement): boolean => {
+        return this.hasUriStartWith(element, 'direct');
+    }
+
+    static hasSedaUri = (element: CamelElement): boolean => {
+        return this.hasUriStartWith(element, 'seda');
+    }
+
+    static hasUriStartWith = (element: CamelElement, text: string): boolean => 
{
+        if ((element as any).uri && typeof (element as any).uri === 'string') {
+            return (element as any).uri.startsWith(text);
+        } else if (element.dslName === 'SagaDefinition') {
+            const completion = (element as SagaDefinition).completion || '';
+            const compensation = (element as SagaDefinition).compensation || 
'';
+            return completion.startsWith(text) || 
compensation.startsWith(text);
+        } else {
+            return false;
+        }
+    }
+
+    static findTopologyRestNodes = (integration: Integration[]): 
TopologyRestNode[] => {
+        const result:TopologyRestNode[] = [];
+        integration.forEach(i => {
+            const filename = i.metadata.name;
+            const routeIds: string[] = [];
+            const routes = i.spec.flows?.filter(flow => flow.dslName === 
'RestDefinition');
+            routes?.forEach((rest: RestDefinition) => {
+                rest?.get?.forEach((d: GetDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                rest?.post?.forEach((d: PostDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                rest?.put?.forEach((d: PutDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                rest?.delete?.forEach((d: DeleteDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                rest?.patch?.forEach((d: PatchDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                rest?.head?.forEach((d: HeadDefinition) => {
+                    if (d.to) routeIds.push(d.to);
+                });
+                const title = 'REST: ' + (rest.description ? rest.description 
: rest.id);
+                result.push(new TopologyRestNode(rest.path || '', '' + 
rest.id, routeIds, title, filename))
+            })
+        })
+        return result;
+    };
+
+    static findTopologyIncomingNodes = (integration: Integration[]): 
TopologyIncomingNode[] => {
+        const result:TopologyIncomingNode[] = [];
+        integration.forEach(i => {
+            const filename = i.metadata.name;
+            const routes = i.spec.flows?.filter(flow => flow.dslName === 
'RouteDefinition');
+            const routeElements = routes?.map(r => {
+                const id = 'incoming-' + r.id;
+                const title = CamelDisplayUtil.getTitle(r.from);
+                const type = TopologyUtils.isElementInternalComponent(r.from) 
? 'internal' : 'external';
+                return new TopologyIncomingNode(id, type, r.id, title, 
filename, r.from);
+            }) || [];
+            result.push(...routeElements)
+        })
+        return result;
+    }
+
+    static findTopologyRouteNodes = (integration: Integration[]): 
TopologyRouteNode[] => {
+        const result:TopologyRouteNode[] = [];
+        integration.forEach(i => {
+            const filename = i.metadata.name;
+            const routes = i.spec.flows?.filter(flow => flow.dslName === 
'RouteDefinition');
+            const routeElements = routes?.map(r => {
+                const id = 'route-' + r.id;
+                const title = '' + (r.description ? r.description : r.id)
+                return new TopologyRouteNode(id, r.id, title, filename, 
r.from);
+            }) || [];
+            result.push(...routeElements)
+        })
+        return result;
+    }
+
+    static findTopologyOutgoingNodes = (integration: Integration[]): 
TopologyOutgoingNode[] => {
+        const result:TopologyOutgoingNode[] = [];
+        integration.forEach(i => {
+            const filename = i.metadata.name;
+            const routes = i.spec.flows?.filter(flow => flow.dslName === 
'RouteDefinition');
+            routes?.forEach(route => {
+                const from: FromDefinition = route.from;
+                const elements = TopologyUtils.findOutgoingInStep(from, []);
+                elements.forEach((e: any) => {
+                    const id = 'outgoing-' + route.id + '-' + e.id;
+                    const title = CamelDisplayUtil.getTitle(e);
+                    const type = TopologyUtils.isElementInternalComponent(e) ? 
'internal' : 'external';
+                    result.push(new TopologyOutgoingNode(id, type, route.id, 
title, filename, e));
+                })
+            })
+
+        })
+        return result;
+    }
+
+    static findOutgoingInStep = (step: CamelElement, result: CamelElement[]): 
CamelElement[] => {
+        if (step !== undefined) {
+            const el = (step as any);
+            if (outgoingDefinitions.includes(el.dslName)) {
+                result.push(step);
+            } else {
+                const childElements = 
CamelDefinitionApiExt.getElementChildrenDefinition(el.dslName);
+                childElements.forEach(child => {
+                    if (child.multiple) {
+                        const sub = (el[child.name] as CamelElement[]);
+                        TopologyUtils.findOutgoingInSteps(sub, result);
+                    } else {
+                        const sub = (el[child.name] as CamelElement);
+                        TopologyUtils.findOutgoingInStep(sub, result);
+                    }
+                })
+            }
+        }
+        return result;
+    }
+
+    static findOutgoingInSteps = (steps: CamelElement[], result: 
CamelElement[]): CamelElement[] => {
+        if (steps !== undefined && steps.length > 0) {
+            steps.forEach(step => TopologyUtils.findOutgoingInStep(step, 
result))
+        }
+        return result;
+    }
+}
diff --git a/karavan-core/src/core/model/TopologyDefinition.ts 
b/karavan-core/src/core/model/TopologyDefinition.ts
new file mode 100644
index 00000000..2ba9142f
--- /dev/null
+++ b/karavan-core/src/core/model/TopologyDefinition.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CamelElement } from './IntegrationDefinition';
+import { FromDefinition } from './CamelDefinition';
+
+export class TopologyRestNode {
+    path: string;
+    id: string;
+    routeIds: string[];
+    title: string;
+    fileName: string;
+
+    constructor(path: string, id: string, routeIds: string[], title: string, 
fileName: string) {
+        this.path = path;
+        this.id = id;
+        this.routeIds = routeIds;
+        this.title = title;
+        this.fileName = fileName;
+    }
+}
+
+export class TopologyIncomingNode {
+    id: string;
+    type: 'internal' | 'external';
+    routeId: string;
+    title: string;
+    fileName: string;
+    from: FromDefinition;
+    
+    constructor(id: string, type: 'internal' | 'external', routeId: string, 
title: string, fileName: string, from: FromDefinition) {
+        this.id = id;
+        this.type = type;
+        this.routeId = routeId;
+        this.title = title;
+        this.fileName = fileName;
+        this.from = from;
+    }
+}
+
+export class TopologyRouteNode {
+    id: string;
+    routeId: string;
+    title: string;
+    fileName: string;
+    from: FromDefinition;
+
+    constructor(id: string, routeId: string, title: string, fileName: string, 
from: FromDefinition) {
+        this.id = id;
+        this.routeId = routeId;
+        this.title = title;
+        this.fileName = fileName;
+        this.from = from;
+    }
+}
+
+export class TopologyOutgoingNode {
+    id: string;
+    type: 'internal' | 'external';
+    routeId: string;
+    title: string;
+    fileName: string;
+    step: CamelElement;
+
+    constructor(id: string, type: 'internal' | 'external', routeId: string, 
title: string, fileName: string, step: CamelElement) {
+        this.id = id;
+        this.type = type
+        this.routeId = routeId;
+        this.title = title;
+        this.fileName = fileName;
+        this.step = step;
+    }
+}
\ No newline at end of file
diff --git a/karavan-core/test/topology.spec.ts 
b/karavan-core/test/topology.spec.ts
new file mode 100644
index 00000000..e76ead2b
--- /dev/null
+++ b/karavan-core/test/topology.spec.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as fs from 'fs';
+import 'mocha';
+import {CamelDefinitionYaml} from "../src/core/api/CamelDefinitionYaml";
+import { TopologyUtils } from '../src/core/api/TopologyUtils';
+
+
+describe('Topology functions', () => {
+
+    it('Topology find', () => {
+        const yaml1 = 
fs.readFileSync('test/topology1.camel.yaml',{encoding:'utf8', flag:'r'});
+        const yaml2 = 
fs.readFileSync('test/topology2.camel.yaml',{encoding:'utf8', flag:'r'});
+        const i1 = CamelDefinitionYaml.yamlToIntegration("test1.yaml", yaml1);
+        const i2 = CamelDefinitionYaml.yamlToIntegration("test1.yaml", yaml2);
+        const tin = TopologyUtils.findTopologyIncomingNodes([i1, i2]);
+        const trn = TopologyUtils.findTopologyRestNodes([i1, i2]);
+        const ton = TopologyUtils.findTopologyOutgoingNodes([i1, i2]);
+        console.log(tin)
+    });
+
+});
diff --git a/karavan-core/test/topology1.camel.yaml 
b/karavan-core/test/topology1.camel.yaml
new file mode 100644
index 00000000..46633080
--- /dev/null
+++ b/karavan-core/test/topology1.camel.yaml
@@ -0,0 +1,56 @@
+- route:
+    id: route-c67f1
+    description: Timer Scheduler
+    from:
+      uri: kamelet:timer-source
+      id: from-b4181
+      parameters:
+        message: Hello
+      steps:
+        - log:
+            message: ${body}
+            id: log-a68a1
+- route:
+    id: route-2cbd1
+    from:
+      uri: kamelet:aws-cloudtrail-source
+      id: from-a3f61
+      parameters:
+        region: fff
+      steps:
+        - to:
+            uri: kamelet:google-pubsub-sink
+            id: to-c27f1
+- route:
+    id: route-de2b1
+    from:
+      uri: amqp
+      id: from-1e661
+      steps:
+        - to:
+            uri: direct
+            id: to-6a041
+            parameters:
+              name: xxxx
+- route:
+    id: route-ec4e1
+    from:
+      uri: direct
+      id: from-e9181
+      parameters:
+        name: xxx
+      steps:
+        - to:
+            uri: kamelet:kafka-not-secured-sink
+            id: to-a47b1
+- rest:
+    id: rest-b499
+    get:
+      - to: direct:xxx
+        id: get-cb63
+    post:
+      - to: direct:xxx
+        id: post-07e4
+    put:
+      - to: direct:xxx
+        id: put-8271
\ No newline at end of file
diff --git a/karavan-core/test/topology2.camel.yaml 
b/karavan-core/test/topology2.camel.yaml
new file mode 100644
index 00000000..49cb6eed
--- /dev/null
+++ b/karavan-core/test/topology2.camel.yaml
@@ -0,0 +1,44 @@
+- route:
+    id: route-c67f
+    from:
+      uri: kamelet:timer-source
+      id: from-b418
+      parameters:
+        message: Hello
+      steps:
+        - log:
+            message: ${body}
+            id: log-a68a
+- route:
+    id: route-2cbd
+    from:
+      uri: kamelet:aws-cloudtrail-source
+      id: from-a3f6
+      parameters:
+        region: fff
+      steps:
+        - to:
+            uri: kamelet:google-pubsub-sink
+            id: to-c27f
+- route:
+    id: route-de2b
+    from:
+      uri: amqp
+      id: from-1e66
+      steps:
+        - to:
+            uri: direct
+            id: to-6a04
+            parameters:
+              name: xxxx
+- route:
+    id: route-ec4e
+    from:
+      uri: direct
+      id: from-e918
+      parameters:
+        name: xxx
+      steps:
+        - to:
+            uri: kamelet:kafka-not-secured-sink
+            id: to-a47b
diff --git a/karavan-designer/package-lock.json 
b/karavan-designer/package-lock.json
index 96126a8c..654708b7 100644
--- a/karavan-designer/package-lock.json
+++ b/karavan-designer/package-lock.json
@@ -13,6 +13,7 @@
         "@patternfly/patternfly": "^5.0.2",
         "@patternfly/react-core": "^5.0.0",
         "@patternfly/react-table": "^5.0.0",
+        "@patternfly/react-topology": "^5.0.0",
         "@types/js-yaml": "4.0.5",
         "@types/node": "18.16.3",
         "@types/uuid": "9.0.1",
@@ -2074,7 +2075,6 @@
       "version": "7.22.15",
       "resolved": 
"https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz";,
       "integrity": 
"sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==",
-      "dev": true,
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -3475,6 +3475,48 @@
       "resolved": 
"https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.0.1.tgz";,
       "integrity": 
"sha512-YafAGJYvxDP4GaQ0vMybalWmx7MJ+etUf1cGoaMh0wRD2eswltT/RckygtEBKR/M61qXbgG+CxKmMyY8leoiDw=="
     },
+    "node_modules/@patternfly/react-topology": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.0.0.tgz";,
+      "integrity": 
"sha512-DW1dXXff5X+5K3ZW8rn1eqSggGfq5My/BMMcyhO6ankgAxAA4uK96/DbWaUMmSxkeHDSF6tD5jTd/SiglQzR1A==",
+      "dependencies": {
+        "@patternfly/react-core": "^5.0.0",
+        "@patternfly/react-icons": "^5.0.0",
+        "@patternfly/react-styles": "^5.0.0",
+        "@types/d3": "^7.4.0",
+        "@types/d3-force": "^1.2.1",
+        "@types/dagre": "0.7.42",
+        "@types/react-measure": "^2.0.6",
+        "d3": "^7.8.0",
+        "dagre": "0.8.2",
+        "lodash": "^4.17.19",
+        "mobx": "^6.9.0",
+        "mobx-react": "^7.6.0",
+        "point-in-svg-path": "^1.0.1",
+        "popper.js": "^1.16.1",
+        "react-measure": "^2.3.0",
+        "tslib": "^2.0.0",
+        "webcola": "3.4.0"
+      },
+      "peerDependencies": {
+        "react": "^17 || ^18",
+        "react-dom": "^17 || ^18"
+      }
+    },
+    "node_modules/@patternfly/react-topology/node_modules/@types/dagre": {
+      "version": "0.7.42",
+      "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.42.tgz";,
+      "integrity": 
"sha512-knVdi1Ul8xYgJ0wdhQ+/2YGJFKJFa/5srcPII9zvOs4KhsHfpnFrSTQXATYmjslglxRMif3Lg+wEZ0beag+94A=="
+    },
+    "node_modules/@patternfly/react-topology/node_modules/dagre": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.2.tgz";,
+      "integrity": 
"sha512-TEOOGZOkCOgCG7AoUIq64sJ3d21SMv8tyoqteLpX+UsUsS9Qw8iap4hhogXY4oB3r0bbZuAjO0atAilgCmsE0Q==",
+      "dependencies": {
+        "graphlib": "^2.1.5",
+        "lodash": "^4.17.4"
+      }
+    },
     "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
       "version": "0.5.11",
       "resolved": 
"https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz";,
@@ -3996,6 +4038,228 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/d3": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz";,
+      "integrity": 
"sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/d3-axis": "*",
+        "@types/d3-brush": "*",
+        "@types/d3-chord": "*",
+        "@types/d3-color": "*",
+        "@types/d3-contour": "*",
+        "@types/d3-delaunay": "*",
+        "@types/d3-dispatch": "*",
+        "@types/d3-drag": "*",
+        "@types/d3-dsv": "*",
+        "@types/d3-ease": "*",
+        "@types/d3-fetch": "*",
+        "@types/d3-force": "*",
+        "@types/d3-format": "*",
+        "@types/d3-geo": "*",
+        "@types/d3-hierarchy": "*",
+        "@types/d3-interpolate": "*",
+        "@types/d3-path": "*",
+        "@types/d3-polygon": "*",
+        "@types/d3-quadtree": "*",
+        "@types/d3-random": "*",
+        "@types/d3-scale": "*",
+        "@types/d3-scale-chromatic": "*",
+        "@types/d3-selection": "*",
+        "@types/d3-shape": "*",
+        "@types/d3-time": "*",
+        "@types/d3-time-format": "*",
+        "@types/d3-timer": "*",
+        "@types/d3-transition": "*",
+        "@types/d3-zoom": "*"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.0.7",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.7.tgz";,
+      "integrity": 
"sha512-4/Q0FckQ8TBjsB0VdGFemJOG8BLXUB2KKlL0VmZ+eOYeOnTb/wDRQqYWpBmQ6IlvWkXwkYiot+n9Px2aTJ7zGQ=="
+    },
+    "node_modules/@types/d3-axis": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.3.tgz";,
+      "integrity": 
"sha512-SE3x/pLO/+GIHH17mvs1uUVPkZ3bHquGzvZpPAh4yadRy71J93MJBpgK/xY8l9gT28yTN1g9v3HfGSFeBMmwZw==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-brush": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.3.tgz";,
+      "integrity": 
"sha512-MQ1/M/B5ifTScHSe5koNkhxn2mhUPqXjGuKjjVYckplAPjP9t2I2sZafb/YVHDwhoXWZoSav+Q726eIbN3qprA==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-chord": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.3.tgz";,
+      "integrity": 
"sha512-keuSRwO02c7PBV3JMWuctIfdeJrVFI7RpzouehvBWL4/GGUB3PBNg/9ZKPZAgJphzmS2v2+7vr7BGDQw1CAulw=="
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz";,
+      "integrity": 
"sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA=="
+    },
+    "node_modules/@types/d3-contour": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.3.tgz";,
+      "integrity": 
"sha512-x7G/tdDZt4m09XZnG2SutbIuQqmkNYqR9uhDMdPlpJbcwepkEjEWG29euFcgVA1k6cn92CHdDL9Z+fOnxnbVQw==",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-delaunay": {
+      "version": "6.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz";,
+      "integrity": 
"sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ=="
+    },
+    "node_modules/@types/d3-dispatch": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.3.tgz";,
+      "integrity": 
"sha512-Df7KW3Re7G6cIpIhQtqHin8yUxUHYAqiE41ffopbmU5+FifYUNV7RVyTg8rQdkEagg83m14QtS8InvNb95Zqug=="
+    },
+    "node_modules/@types/d3-drag": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.3.tgz";,
+      "integrity": 
"sha512-82AuQMpBQjuXeIX4tjCYfWjpm3g7aGCfx6dFlxX2JlRaiME/QWcHzBsINl7gbHCODA2anPYlL31/Trj/UnjK9A==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.2.tgz";,
+      "integrity": 
"sha512-DooW5AOkj4AGmseVvbwHvwM/Ltu0Ks0WrhG6r5FG9riHT5oUUTHz6xHsHqJSVU8ZmPkOqlUEY2obS5C9oCIi2g=="
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz";,
+      "integrity": 
"sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA=="
+    },
+    "node_modules/@types/d3-fetch": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.3.tgz";,
+      "integrity": 
"sha512-/EsDKRiQkby3Z/8/AiZq8bsuLDo/tYHnNIZkUpSeEHWV7fHUl6QFBjvMPbhkKGk9jZutzfOkGygCV7eR/MkcXA==",
+      "dependencies": {
+        "@types/d3-dsv": "*"
+      }
+    },
+    "node_modules/@types/d3-force": {
+      "version": "1.2.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz";,
+      "integrity": 
"sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w=="
+    },
+    "node_modules/@types/d3-format": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz";,
+      "integrity": 
"sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg=="
+    },
+    "node_modules/@types/d3-geo": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.4.tgz";,
+      "integrity": 
"sha512-kmUK8rVVIBPKJ1/v36bk2aSgwRj2N/ZkjDT+FkMT5pgedZoPlyhaG62J+9EgNIgUXE6IIL0b7bkLxCzhE6U4VQ==",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-hierarchy": {
+      "version": "3.1.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.3.tgz";,
+      "integrity": 
"sha512-GpSK308Xj+HeLvogfEc7QsCOcIxkDwLhFYnOoohosEzOqv7/agxwvJER1v/kTC+CY1nfazR0F7gnHo7GE41/fw=="
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz";,
+      "integrity": 
"sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz";,
+      "integrity": 
"sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg=="
+    },
+    "node_modules/@types/d3-polygon": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz";,
+      "integrity": 
"sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw=="
+    },
+    "node_modules/@types/d3-quadtree": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz";,
+      "integrity": 
"sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw=="
+    },
+    "node_modules/@types/d3-random": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz";,
+      "integrity": 
"sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ=="
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.4.tgz";,
+      "integrity": 
"sha512-eq1ZeTj0yr72L8MQk6N6heP603ubnywSDRfNpi5enouR112HzGLS6RIvExCzZTraFF4HdzNpJMwA/zGiMoHUUw==",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz";,
+      "integrity": 
"sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw=="
+    },
+    "node_modules/@types/d3-selection": {
+      "version": "3.0.6",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.6.tgz";,
+      "integrity": 
"sha512-2ACr96USZVjXR9KMD9IWi1Epo4rSDKnUtYn6q2SPhYxykvXTw9vR77lkFNruXVg4i1tzQtBxeDMx0oNvJWbF1w=="
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.2",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.2.tgz";,
+      "integrity": 
"sha512-NN4CXr3qeOUNyK5WasVUV8NCSAx/CRVcwcb0BuuS1PiTqwIm6ABi1SyasLZ/vsVCFDArF+W4QiGzSry1eKYQ7w==",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz";,
+      "integrity": 
"sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
+    },
+    "node_modules/@types/d3-time-format": {
+      "version": "4.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz";,
+      "integrity": 
"sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw=="
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz";,
+      "integrity": 
"sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g=="
+    },
+    "node_modules/@types/d3-transition": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.4.tgz";,
+      "integrity": 
"sha512-512a4uCOjUzsebydItSXsHrPeQblCVk8IKjqCUmrlvBWkkVh3donTTxmURDo1YPwIVDh5YVwCAO6gR4sgimCPQ==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.4.tgz";,
+      "integrity": 
"sha512-cqkuY1ah9ZQre2POqjSLcM8g40UVya/qwEUrNYP2/rCVljbmqKCVcv+ebvwhlI5azIbSEL7m+os6n+WlYA43aA==",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
     "node_modules/@types/dagre": {
       "version": "0.7.49",
       "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.49.tgz";,
@@ -4052,6 +4316,11 @@
         "@types/send": "*"
       }
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.10",
+      "resolved": 
"https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz";,
+      "integrity": 
"sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
+    },
     "node_modules/@types/graceful-fs": {
       "version": "4.1.6",
       "resolved": 
"https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz";,
@@ -4149,8 +4418,7 @@
     "node_modules/@types/prop-types": {
       "version": "15.7.5",
       "resolved": 
"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz";,
-      "integrity": 
"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
-      "devOptional": true
+      "integrity": 
"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
     },
     "node_modules/@types/qs": {
       "version": "6.9.8",
@@ -4168,7 +4436,6 @@
       "version": "18.2.21",
       "resolved": 
"https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz";,
       "integrity": 
"sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
-      "devOptional": true,
       "dependencies": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -4184,6 +4451,14 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-measure": {
+      "version": "2.0.8",
+      "resolved": 
"https://registry.npmjs.org/@types/react-measure/-/react-measure-2.0.8.tgz";,
+      "integrity": 
"sha512-Pu4/hQ/1AKVN6efoawtcM+l376WYOI8e1fiM6ir4pdLkHilDCkJLjUGvAm0mWKJ0GE6hzu55yCrcJ/xNyEdFwA==",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/resolve": {
       "version": "1.17.1",
       "resolved": 
"https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz";,
@@ -4202,8 +4477,7 @@
     "node_modules/@types/scheduler": {
       "version": "0.16.3",
       "resolved": 
"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz";,
-      "integrity": 
"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
-      "devOptional": true
+      "integrity": 
"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
     },
     "node_modules/@types/semver": {
       "version": "7.5.1",
@@ -6755,8 +7029,385 @@
     "node_modules/csstype": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz";,
-      "integrity": 
"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
-      "devOptional": true
+      "integrity": 
"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+    },
+    "node_modules/d3": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz";,
+      "integrity": 
"sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "dependencies": {
+        "d3-array": "3",
+        "d3-axis": "3",
+        "d3-brush": "3",
+        "d3-chord": "3",
+        "d3-color": "3",
+        "d3-contour": "4",
+        "d3-delaunay": "6",
+        "d3-dispatch": "3",
+        "d3-drag": "3",
+        "d3-dsv": "3",
+        "d3-ease": "3",
+        "d3-fetch": "3",
+        "d3-force": "3",
+        "d3-format": "3",
+        "d3-geo": "3",
+        "d3-hierarchy": "3",
+        "d3-interpolate": "3",
+        "d3-path": "3",
+        "d3-polygon": "3",
+        "d3-quadtree": "3",
+        "d3-random": "3",
+        "d3-scale": "4",
+        "d3-scale-chromatic": "3",
+        "d3-selection": "3",
+        "d3-shape": "3",
+        "d3-time": "3",
+        "d3-time-format": "4",
+        "d3-timer": "3",
+        "d3-transition": "3",
+        "d3-zoom": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz";,
+      "integrity": 
"sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-axis": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz";,
+      "integrity": 
"sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz";,
+      "integrity": 
"sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-chord": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz";,
+      "integrity": 
"sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+      "dependencies": {
+        "d3-path": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz";,
+      "integrity": 
"sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-contour": {
+      "version": "4.0.2",
+      "resolved": 
"https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz";,
+      "integrity": 
"sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+      "dependencies": {
+        "d3-array": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": 
"https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz";,
+      "integrity": 
"sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "dependencies": {
+        "delaunator": "5"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz";,
+      "integrity": 
"sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz";,
+      "integrity": 
"sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz";,
+      "integrity": 
"sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz";,
+      "integrity": 
"sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz";,
+      "integrity": 
"sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz";,
+      "integrity": 
"sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz";,
+      "integrity": 
"sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz";,
+      "integrity": 
"sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz";,
+      "integrity": 
"sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "dependencies": {
+        "d3-array": "2.5.0 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": 
"https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz";,
+      "integrity": 
"sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz";,
+      "integrity": 
"sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz";,
+      "integrity": 
"sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-polygon": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz";,
+      "integrity": 
"sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz";,
+      "integrity": 
"sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz";,
+      "integrity": 
"sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz";,
+      "integrity": 
"sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz";,
+      "integrity": 
"sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz";,
+      "integrity": 
"sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz";,
+      "integrity": 
"sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz";,
+      "integrity": 
"sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": 
"https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz";,
+      "integrity": 
"sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz";,
+      "integrity": 
"sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz";,
+      "integrity": 
"sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz";,
+      "integrity": 
"sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
     },
     "node_modules/dagre": {
       "version": "0.8.5",
@@ -6868,6 +7519,14 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/delaunator": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz";,
+      "integrity": 
"sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+      "dependencies": {
+        "robust-predicates": "^3.0.0"
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": 
"https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz";,
@@ -8924,6 +9583,11 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/get-node-dimensions": {
+      "version": "1.2.1",
+      "resolved": 
"https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz";,
+      "integrity": 
"sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ=="
+    },
     "node_modules/get-own-enumerable-property-symbols": {
       "version": "3.0.2",
       "resolved": 
"https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz";,
@@ -9510,7 +10174,6 @@
       "version": "0.6.3",
       "resolved": 
"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz";,
       "integrity": 
"sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
-      "dev": true,
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
@@ -9647,6 +10310,14 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz";,
+      "integrity": 
"sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/ipaddr.js": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz";,
@@ -13039,6 +13710,60 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/mobx": {
+      "version": "6.10.2",
+      "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.10.2.tgz";,
+      "integrity": 
"sha512-B1UGC3ieK3boCjnMEcZSwxqRDMdzX65H/8zOHbuTY8ZhvrIjTUoLRR2TP2bPqIgYRfb3+dUigu8yMZufNjn0LQ==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      }
+    },
+    "node_modules/mobx-react": {
+      "version": "7.6.0",
+      "resolved": 
"https://registry.npmjs.org/mobx-react/-/mobx-react-7.6.0.tgz";,
+      "integrity": 
"sha512-+HQUNuh7AoQ9ZnU6c4rvbiVVl+wEkb9WqYsVDzGLng+Dqj1XntHu79PvEWKtSMoMj67vFp/ZPXcElosuJO8ckA==",
+      "dependencies": {
+        "mobx-react-lite": "^3.4.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      },
+      "peerDependencies": {
+        "mobx": "^6.1.0",
+        "react": "^16.8.0 || ^17 || ^18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mobx-react-lite": {
+      "version": "3.4.3",
+      "resolved": 
"https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz";,
+      "integrity": 
"sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      },
+      "peerDependencies": {
+        "mobx": "^6.1.0",
+        "react": "^16.8.0 || ^17 || ^18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/monaco-editor": {
       "version": "0.43.0",
       "resolved": 
"https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.43.0.tgz";,
@@ -13765,6 +14490,24 @@
         "node": ">=4"
       }
     },
+    "node_modules/point-in-svg-path": {
+      "version": "1.0.2",
+      "resolved": 
"https://registry.npmjs.org/point-in-svg-path/-/point-in-svg-path-1.0.2.tgz";,
+      "integrity": 
"sha512-+Smsf7B9e7eRFHIwpN+4rE8inF2APbFWeywPfUgbeh02xdJSkbTz6Pqdt7A36wVCR+CnLbaNkRnBjgOpF5RMVQ==",
+      "engines": {
+        "node": ">=8.x.x"
+      }
+    },
+    "node_modules/popper.js": {
+      "version": "1.16.1",
+      "resolved": 
"https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz";,
+      "integrity": 
"sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
+      "deprecated": "You can find the new Popper v2 at @popperjs/core, this 
package is dedicated to the legacy v1",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs";
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.29",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz";,
@@ -15528,6 +16271,21 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz";,
       "integrity": 
"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "node_modules/react-measure": {
+      "version": "2.5.2",
+      "resolved": 
"https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz";,
+      "integrity": 
"sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==",
+      "dependencies": {
+        "@babel/runtime": "^7.2.0",
+        "get-node-dimensions": "^1.2.1",
+        "prop-types": "^15.6.2",
+        "resize-observer-polyfill": "^1.5.0"
+      },
+      "peerDependencies": {
+        "react": ">0.13.0",
+        "react-dom": ">0.13.0"
+      }
+    },
     "node_modules/react-refresh": {
       "version": "0.11.0",
       "resolved": 
"https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz";,
@@ -15731,8 +16489,7 @@
     "node_modules/regenerator-runtime": {
       "version": "0.14.0",
       "resolved": 
"https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz";,
-      "integrity": 
"sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
-      "dev": true
+      "integrity": 
"sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
     },
     "node_modules/regenerator-transform": {
       "version": "0.15.2",
@@ -15850,6 +16607,11 @@
       "integrity": 
"sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
       "dev": true
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": 
"https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz";,
+      "integrity": 
"sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+    },
     "node_modules/resolve": {
       "version": "1.22.4",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz";,
@@ -16000,6 +16762,11 @@
         "url": "https://github.com/sponsors/isaacs";
       }
     },
+    "node_modules/robust-predicates": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz";,
+      "integrity": 
"sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+    },
     "node_modules/rollup": {
       "version": "2.79.1",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz";,
@@ -16098,6 +16865,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz";,
+      "integrity": 
"sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
     "node_modules/rxjs": {
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz";,
@@ -16161,8 +16933,7 @@
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": 
"https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz";,
-      "integrity": 
"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": 
"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/sanitize.css": {
       "version": "13.0.0",
@@ -17838,6 +18609,54 @@
         "minimalistic-assert": "^1.0.0"
       }
     },
+    "node_modules/webcola": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/webcola/-/webcola-3.4.0.tgz";,
+      "integrity": 
"sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==",
+      "dependencies": {
+        "d3-dispatch": "^1.0.3",
+        "d3-drag": "^1.0.4",
+        "d3-shape": "^1.3.5",
+        "d3-timer": "^1.0.5"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-dispatch": {
+      "version": "1.0.6",
+      "resolved": 
"https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz";,
+      "integrity": 
"sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
+    },
+    "node_modules/webcola/node_modules/d3-drag": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz";,
+      "integrity": 
"sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
+      "dependencies": {
+        "d3-dispatch": "1",
+        "d3-selection": "1"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-path": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz";,
+      "integrity": 
"sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+    },
+    "node_modules/webcola/node_modules/d3-selection": {
+      "version": "1.4.2",
+      "resolved": 
"https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz";,
+      "integrity": 
"sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg=="
+    },
+    "node_modules/webcola/node_modules/d3-shape": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz";,
+      "integrity": 
"sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+      "dependencies": {
+        "d3-path": "1"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-timer": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz";,
+      "integrity": 
"sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
+    },
     "node_modules/webidl-conversions": {
       "version": "6.1.0",
       "resolved": 
"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz";,
diff --git a/karavan-designer/package.json b/karavan-designer/package.json
index 92534f5b..47cec418 100644
--- a/karavan-designer/package.json
+++ b/karavan-designer/package.json
@@ -30,6 +30,7 @@
     "@patternfly/patternfly": "^5.0.2",
     "@patternfly/react-core": "^5.0.0",
     "@patternfly/react-table": "^5.0.0",
+    "@patternfly/react-topology": "^5.0.0",
     "@types/js-yaml": "4.0.5",
     "@types/node": "18.16.3",
     "@types/uuid": "9.0.1",
diff --git a/karavan-designer/src/designer/route/DslConnections.tsx 
b/karavan-designer/src/designer/route/DslConnections.tsx
index c9d85ccb..44f23042 100644
--- a/karavan-designer/src/designer/route/DslConnections.tsx
+++ b/karavan-designer/src/designer/route/DslConnections.tsx
@@ -23,9 +23,9 @@ import {SagaDefinition} from 
"karavan-core/lib/model/CamelDefinition";
 import {useConnectionsStore, useDesignerStore, useIntegrationStore} from 
"../KaravanStore";
 import {shallow} from "zustand/shallow";
 import {CamelDefinitionApiExt} from 
"karavan-core/lib/api/CamelDefinitionApiExt";
+import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
 
 const overlapGap: number = 40;
-const outgoingDefinitions: string[] = ['ToDefinition', 'KameletDefinition', 
'ToDynamicDefinition', "PollEnrichDefinition", "EnrichDefinition", 
"WireTapDefinition", "SagaDefinition"];
 
 export function DslConnections() {
 
@@ -61,8 +61,8 @@ export function DslConnections() {
     function getIncomings() {
         let outs: [string, number][] = Array.from(steps.values())
             .filter(pos => ["FromDefinition"].includes(pos.step.dslName))
-            .filter(pos => !CamelUi.isElementInternalComponent(pos.step))
-            .filter(pos => !(pos.step.dslName === 'FromDefinition' && 
CamelUi.hasInternalUri(pos.step)))
+            .filter(pos => !TopologyUtils.isElementInternalComponent(pos.step))
+            .filter(pos => !(pos.step.dslName === 'FromDefinition' && 
TopologyUtils.hasInternalUri(pos.step)))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                 const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
@@ -135,11 +135,12 @@ export function DslConnections() {
 
 
     function getOutgoings(): [string, number][] {
+        const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
         let outs: [string, number][] = Array.from(steps.values())
             .filter(pos => outgoingDefinitions.includes(pos.step.dslName))
             .filter(pos => pos.step.dslName !== 'KameletDefinition' || 
(pos.step.dslName === 'KameletDefinition' && 
!CamelUi.isActionKamelet(pos.step)))
-            .filter(pos => pos.step.dslName === 'ToDefinition' && 
!CamelUi.isActionKamelet(pos.step) && 
!CamelUi.isElementInternalComponent(pos.step))
-            .filter(pos => !(outgoingDefinitions.includes(pos.step.dslName) && 
CamelUi.hasInternalUri(pos.step)))
+            .filter(pos => pos.step.dslName === 'ToDefinition' && 
!CamelUi.isActionKamelet(pos.step) && 
!TopologyUtils.isElementInternalComponent(pos.step))
+            .filter(pos => !(outgoingDefinitions.includes(pos.step.dslName) && 
TopologyUtils.hasInternalUri(pos.step)))
             .filter(pos => pos.step.dslName !== 'SagaDefinition')
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
@@ -203,8 +204,9 @@ export function DslConnections() {
     }
 
     function getInternals(): [string, number, boolean][] {
+        const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
         let outs: [string, number, boolean][] = Array.from(steps.values())
-            .filter(pos => outgoingDefinitions.includes(pos.step.dslName) && 
CamelUi.hasInternalUri(pos.step))
+            .filter(pos => outgoingDefinitions.includes(pos.step.dslName) && 
TopologyUtils.hasInternalUri(pos.step))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                 const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
@@ -222,7 +224,7 @@ export function DslConnections() {
             const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left;
             const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top;
             const r = pos.headerRect.height / 2;
-            const className = (CamelUi.hasDirectUri(pos.step) ? "path-direct" 
: "path-seda") + (data[2] ? "-selected" : "");
+            const className = (TopologyUtils.hasDirectUri(pos.step) ? 
"path-direct" : "path-seda") + (data[2] ? "-selected" : "");
             return getInternalLine(uri, key, className, fromX, fromY, r, 
data[1]);
         } else if (pos?.step.dslName === 'SagaDefinition'){
             const saga = (pos?.step as SagaDefinition);
diff --git a/karavan-designer/src/designer/route/DslElement.tsx 
b/karavan-designer/src/designer/route/DslElement.tsx
index 9aaf6735..489c675d 100644
--- a/karavan-designer/src/designer/route/DslElement.tsx
+++ b/karavan-designer/src/designer/route/DslElement.tsx
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, {CSSProperties, useEffect, useMemo, useRef, useState} from 
'react';
+import React, {CSSProperties, useMemo, useState} from 'react';
 import {
     Button,
     Flex,
@@ -377,7 +377,7 @@ export function DslElement(props: Props) {
         if (hideAddButton) return (<></>)
         else return (
             <Tooltip position={"bottom"}
-                     content={<div>{"Add step to " + 
CamelUi.getTitle(step)}</div>}>
+                     content={<div>{"Add step to " + 
CamelDisplayUtil.getTitle(step)}</div>}>
                 <button type="button" aria-label="Add" onClick={e => 
onOpenSelector(e)}
                         className={isAddStepButtonLeft() ? "add-button 
add-button-left" : "add-button add-button-bottom"}>
                     <AddIcon/>
@@ -388,7 +388,7 @@ export function DslElement(props: Props) {
 
     function getAddElementButton() {
         return (
-            <Tooltip position={"bottom"} content={<div>{"Add DSL element to " 
+ CamelUi.getTitle(props.step)}</div>}>
+            <Tooltip position={"bottom"} content={<div>{"Add DSL element to " 
+ CamelDisplayUtil.getTitle(props.step)}</div>}>
                 <button
                     type="button"
                     aria-label="Add"
diff --git a/karavan-designer/src/designer/route/DslProperties.tsx 
b/karavan-designer/src/designer/route/DslProperties.tsx
index 337e12c0..69729121 100644
--- a/karavan-designer/src/designer/route/DslProperties.tsx
+++ b/karavan-designer/src/designer/route/DslProperties.tsx
@@ -33,6 +33,7 @@ import CloneIcon from 
"@patternfly/react-icons/dist/esm/icons/clone-icon";
 import {useDesignerStore, useIntegrationStore} from "../KaravanStore";
 import {shallow} from "zustand/shallow";
 import {usePropertiesHook} from "./usePropertiesHook";
+import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
 
 interface Props {
     isRouteDesigner: boolean
@@ -52,7 +53,7 @@ export function DslProperties(props: Props) {
     const [isDescriptionExpanded, setIsDescriptionExpanded] = 
useState<boolean>(false);
 
     function getRouteHeader(): JSX.Element {
-        const title = selectedStep && CamelUi.getTitle(selectedStep)
+        const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep)
         const description = selectedStep && 
CamelUi.getDescription(selectedStep);
         const descriptionLines: string [] = description ? 
description?.split("\n") : [""];
         return (
@@ -73,7 +74,7 @@ export function DslProperties(props: Props) {
     }
 
     function getClonableElementHeader(): JSX.Element {
-        const title = selectedStep && CamelUi.getTitle(selectedStep);
+        const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep);
         const description = selectedStep?.dslName ? 
CamelMetadataApi.getCamelModelMetadataByClassName(selectedStep?.dslName)?.description
 : title;
         const descriptionLines: string [] = description ? 
description?.split("\n") : [""];
         return (
diff --git a/karavan-designer/src/designer/utils/CamelUi.tsx 
b/karavan-designer/src/designer/utils/CamelUi.tsx
index c5d63801..b3a098c7 100644
--- a/karavan-designer/src/designer/utils/CamelUi.tsx
+++ b/karavan-designer/src/designer/utils/CamelUi.tsx
@@ -88,6 +88,8 @@ import {
     WorkflowIcon
 } from "./KaravanIcons";
 import React from "react";
+import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
+import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
 
 const StepElements: string[] = [
     "AggregateDefinition",
@@ -300,30 +302,6 @@ export class CamelUi {
         else return false;
     }
 
-    static hasInternalUri = (element: CamelElement): boolean => {
-        return this.hasDirectUri(element) || this.hasSedaUri(element);
-    }
-
-    static hasDirectUri = (element: CamelElement): boolean => {
-        return this.hasUriStartWith(element, 'direct');
-    }
-
-    static hasSedaUri = (element: CamelElement): boolean => {
-        return this.hasUriStartWith(element, 'seda');
-    }
-
-    static hasUriStartWith = (element: CamelElement, text: string): boolean => 
{
-        if ((element as any).uri && typeof (element as any).uri === 'string') {
-            return (element as any).uri.startsWith(text);
-        } else if (element.dslName === 'SagaDefinition') {
-            const completion = (element as SagaDefinition).completion || '';
-            const compensation = (element as SagaDefinition).compensation || 
'';
-            return completion.startsWith(text) || 
compensation.startsWith(text);
-        } else {
-            return false;
-        }
-    }
-
     static getInternalRouteUris = (integration: Integration, componentName: 
string, showComponentName: boolean = true): string[] => {
         const result: string[] = [];
         integration.spec.flows?.filter(f => f.dslName === 'RouteDefinition')
@@ -351,22 +329,6 @@ export class CamelUi {
         }
     }
 
-    static getTitle = (element: CamelElement): string => {
-        const k: KameletModel | undefined = CamelUtil.getKamelet(element);
-        if (k) {
-            return k.title();
-        } else if (element.dslName === 'RouteDefinition') {
-            const routeId = (element as RouteDefinition).id
-            return routeId ? routeId : CamelUtil.capitalizeName((element as 
any).stepName);
-        } else if ((element as any).uri && (['ToDefinition', 
'FromDefinition'].includes(element.dslName))) {
-            const uri = (element as any).uri
-            return ComponentApi.getComponentTitleFromUri(uri) || '';
-        } else {
-            const title = 
CamelMetadataApi.getCamelModelMetadataByClassName(element.dslName);
-            return title ? title.title : CamelUtil.capitalizeName((element as 
any).stepName);
-        }
-    }
-
     static getDescription = (element: CamelElement): string => {
         const kamelet: KameletModel | undefined = 
CamelUtil.getKamelet(element);
         if (kamelet) {
@@ -376,7 +338,7 @@ export class CamelUi {
             return ComponentApi.getComponentDescriptionFromUri(uri) || '';
         } else {
             const description = 
CamelMetadataApi.getCamelModelMetadataByClassName(element.dslName)?.description;
-            return description ? description : CamelUi.getTitle(element);
+            return description ? description : 
CamelDisplayUtil.getTitle(element);
         }
     }
 
@@ -669,43 +631,17 @@ export class CamelUi {
         }
     }
 
-    static isElementInternalComponent = (element: CamelElement): boolean => {
-        const uri = (element as any).uri;
-        const component = ComponentApi.findByName(uri);
-        return component !== undefined && 
CamelUi.isComponentInternal(component.component.label);
-    }
-
-    static isComponentInternal = (label: string): boolean => {
-        const labels = label.split(",");
-        if (labels.includes('core') && (
-            labels.includes('transformation')
-            || labels.includes('testing')
-            || labels.includes('scheduling')
-            || labels.includes('monitoring')
-            || labels.includes('transformation')
-            || labels.includes('java')
-            || labels.includes('endpoint')
-            || labels.includes('script')
-            || labels.includes('validation')
-        )) {
-            return true;
-        } else if (label === 'transformation') {
-            return true;
-        }
-        return false;
-    }
-
     static getIconForElement = (element: CamelElement): JSX.Element => {
         const uri = (element as any).uri;
         const component = ComponentApi.findByName(uri);
         const k: KameletModel | undefined = CamelUtil.getKamelet(element);
         if (["FromDefinition", "KameletDefinition"].includes(element.dslName) 
&& k !== undefined) {
             return k ? this.getIconFromSource(k.icon()) : 
CamelUi.getIconForDslName(element.dslName);
-        } else if ("FromDefinition" === element.dslName && component !== 
undefined && CamelUi.isComponentInternal(component.component.label)) {
+        } else if ("FromDefinition" === element.dslName && component !== 
undefined && TopologyUtils.isComponentInternal(component.component.label)) {
             return this.getIconForComponent(component?.component.title, 
component?.component.label);
         } else if (element.dslName === "ToDefinition" && (element as 
ToDefinition).uri?.startsWith("kamelet:")) {
             return k ? this.getIconFromSource(k.icon()) : 
CamelUi.getIconForDslName(element.dslName);
-        } else if (element.dslName === "ToDefinition" && component && 
CamelUi.isComponentInternal(component.component.label)) {
+        } else if (element.dslName === "ToDefinition" && component && 
TopologyUtils.isComponentInternal(component.component.label)) {
             return this.getIconForComponent(component?.component.title, 
component?.component.label);
         } else {
             return this.getIconForDslName(element.dslName);
diff --git a/karavan-web/karavan-app/src/main/webui/package-lock.json 
b/karavan-web/karavan-app/src/main/webui/package-lock.json
index 6af49aad..a87a8bcd 100644
--- a/karavan-web/karavan-app/src/main/webui/package-lock.json
+++ b/karavan-web/karavan-app/src/main/webui/package-lock.json
@@ -15,6 +15,7 @@
         "@patternfly/react-core": "^5.0.0",
         "@patternfly/react-log-viewer": "^5.0.0",
         "@patternfly/react-table": "^5.0.0",
+        "@patternfly/react-topology": "^5.0.0",
         "@types/js-yaml": "4.0.5",
         "@types/node": "18.16.3",
         "@types/uuid": "9.0.1",
@@ -2081,7 +2082,6 @@
       "version": "7.22.15",
       "resolved": 
"https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz";,
       "integrity": 
"sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==",
-      "dev": true,
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -3535,6 +3535,48 @@
       "resolved": 
"https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.0.1.tgz";,
       "integrity": 
"sha512-YafAGJYvxDP4GaQ0vMybalWmx7MJ+etUf1cGoaMh0wRD2eswltT/RckygtEBKR/M61qXbgG+CxKmMyY8leoiDw=="
     },
+    "node_modules/@patternfly/react-topology": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.0.0.tgz";,
+      "integrity": 
"sha512-DW1dXXff5X+5K3ZW8rn1eqSggGfq5My/BMMcyhO6ankgAxAA4uK96/DbWaUMmSxkeHDSF6tD5jTd/SiglQzR1A==",
+      "dependencies": {
+        "@patternfly/react-core": "^5.0.0",
+        "@patternfly/react-icons": "^5.0.0",
+        "@patternfly/react-styles": "^5.0.0",
+        "@types/d3": "^7.4.0",
+        "@types/d3-force": "^1.2.1",
+        "@types/dagre": "0.7.42",
+        "@types/react-measure": "^2.0.6",
+        "d3": "^7.8.0",
+        "dagre": "0.8.2",
+        "lodash": "^4.17.19",
+        "mobx": "^6.9.0",
+        "mobx-react": "^7.6.0",
+        "point-in-svg-path": "^1.0.1",
+        "popper.js": "^1.16.1",
+        "react-measure": "^2.3.0",
+        "tslib": "^2.0.0",
+        "webcola": "3.4.0"
+      },
+      "peerDependencies": {
+        "react": "^17 || ^18",
+        "react-dom": "^17 || ^18"
+      }
+    },
+    "node_modules/@patternfly/react-topology/node_modules/@types/dagre": {
+      "version": "0.7.42",
+      "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.42.tgz";,
+      "integrity": 
"sha512-knVdi1Ul8xYgJ0wdhQ+/2YGJFKJFa/5srcPII9zvOs4KhsHfpnFrSTQXATYmjslglxRMif3Lg+wEZ0beag+94A=="
+    },
+    "node_modules/@patternfly/react-topology/node_modules/dagre": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.2.tgz";,
+      "integrity": 
"sha512-TEOOGZOkCOgCG7AoUIq64sJ3d21SMv8tyoqteLpX+UsUsS9Qw8iap4hhogXY4oB3r0bbZuAjO0atAilgCmsE0Q==",
+      "dependencies": {
+        "graphlib": "^2.1.5",
+        "lodash": "^4.17.4"
+      }
+    },
     "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
       "version": "0.5.11",
       "resolved": 
"https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz";,
@@ -4064,21 +4106,142 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/d3": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz";,
+      "integrity": 
"sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/d3-axis": "*",
+        "@types/d3-brush": "*",
+        "@types/d3-chord": "*",
+        "@types/d3-color": "*",
+        "@types/d3-contour": "*",
+        "@types/d3-delaunay": "*",
+        "@types/d3-dispatch": "*",
+        "@types/d3-drag": "*",
+        "@types/d3-dsv": "*",
+        "@types/d3-ease": "*",
+        "@types/d3-fetch": "*",
+        "@types/d3-force": "*",
+        "@types/d3-format": "*",
+        "@types/d3-geo": "*",
+        "@types/d3-hierarchy": "*",
+        "@types/d3-interpolate": "*",
+        "@types/d3-path": "*",
+        "@types/d3-polygon": "*",
+        "@types/d3-quadtree": "*",
+        "@types/d3-random": "*",
+        "@types/d3-scale": "*",
+        "@types/d3-scale-chromatic": "*",
+        "@types/d3-selection": "*",
+        "@types/d3-shape": "*",
+        "@types/d3-time": "*",
+        "@types/d3-time-format": "*",
+        "@types/d3-timer": "*",
+        "@types/d3-transition": "*",
+        "@types/d3-zoom": "*"
+      }
+    },
     "node_modules/@types/d3-array": {
       "version": "3.0.7",
       "resolved": 
"https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.7.tgz";,
       "integrity": 
"sha512-4/Q0FckQ8TBjsB0VdGFemJOG8BLXUB2KKlL0VmZ+eOYeOnTb/wDRQqYWpBmQ6IlvWkXwkYiot+n9Px2aTJ7zGQ=="
     },
+    "node_modules/@types/d3-axis": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.3.tgz";,
+      "integrity": 
"sha512-SE3x/pLO/+GIHH17mvs1uUVPkZ3bHquGzvZpPAh4yadRy71J93MJBpgK/xY8l9gT28yTN1g9v3HfGSFeBMmwZw==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-brush": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.3.tgz";,
+      "integrity": 
"sha512-MQ1/M/B5ifTScHSe5koNkhxn2mhUPqXjGuKjjVYckplAPjP9t2I2sZafb/YVHDwhoXWZoSav+Q726eIbN3qprA==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-chord": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.3.tgz";,
+      "integrity": 
"sha512-keuSRwO02c7PBV3JMWuctIfdeJrVFI7RpzouehvBWL4/GGUB3PBNg/9ZKPZAgJphzmS2v2+7vr7BGDQw1CAulw=="
+    },
     "node_modules/@types/d3-color": {
       "version": "3.1.0",
       "resolved": 
"https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz";,
       "integrity": 
"sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA=="
     },
+    "node_modules/@types/d3-contour": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.3.tgz";,
+      "integrity": 
"sha512-x7G/tdDZt4m09XZnG2SutbIuQqmkNYqR9uhDMdPlpJbcwepkEjEWG29euFcgVA1k6cn92CHdDL9Z+fOnxnbVQw==",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-delaunay": {
+      "version": "6.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz";,
+      "integrity": 
"sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ=="
+    },
+    "node_modules/@types/d3-dispatch": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.3.tgz";,
+      "integrity": 
"sha512-Df7KW3Re7G6cIpIhQtqHin8yUxUHYAqiE41ffopbmU5+FifYUNV7RVyTg8rQdkEagg83m14QtS8InvNb95Zqug=="
+    },
+    "node_modules/@types/d3-drag": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.3.tgz";,
+      "integrity": 
"sha512-82AuQMpBQjuXeIX4tjCYfWjpm3g7aGCfx6dFlxX2JlRaiME/QWcHzBsINl7gbHCODA2anPYlL31/Trj/UnjK9A==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.2.tgz";,
+      "integrity": 
"sha512-DooW5AOkj4AGmseVvbwHvwM/Ltu0Ks0WrhG6r5FG9riHT5oUUTHz6xHsHqJSVU8ZmPkOqlUEY2obS5C9oCIi2g=="
+    },
     "node_modules/@types/d3-ease": {
       "version": "3.0.0",
       "resolved": 
"https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz";,
       "integrity": 
"sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA=="
     },
+    "node_modules/@types/d3-fetch": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.3.tgz";,
+      "integrity": 
"sha512-/EsDKRiQkby3Z/8/AiZq8bsuLDo/tYHnNIZkUpSeEHWV7fHUl6QFBjvMPbhkKGk9jZutzfOkGygCV7eR/MkcXA==",
+      "dependencies": {
+        "@types/d3-dsv": "*"
+      }
+    },
+    "node_modules/@types/d3-force": {
+      "version": "1.2.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz";,
+      "integrity": 
"sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w=="
+    },
+    "node_modules/@types/d3-format": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz";,
+      "integrity": 
"sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg=="
+    },
+    "node_modules/@types/d3-geo": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.4.tgz";,
+      "integrity": 
"sha512-kmUK8rVVIBPKJ1/v36bk2aSgwRj2N/ZkjDT+FkMT5pgedZoPlyhaG62J+9EgNIgUXE6IIL0b7bkLxCzhE6U4VQ==",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-hierarchy": {
+      "version": "3.1.3",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.3.tgz";,
+      "integrity": 
"sha512-GpSK308Xj+HeLvogfEc7QsCOcIxkDwLhFYnOoohosEzOqv7/agxwvJER1v/kTC+CY1nfazR0F7gnHo7GE41/fw=="
+    },
     "node_modules/@types/d3-interpolate": {
       "version": "3.0.1",
       "resolved": 
"https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz";,
@@ -4092,6 +4255,21 @@
       "resolved": 
"https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz";,
       "integrity": 
"sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg=="
     },
+    "node_modules/@types/d3-polygon": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz";,
+      "integrity": 
"sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw=="
+    },
+    "node_modules/@types/d3-quadtree": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz";,
+      "integrity": 
"sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw=="
+    },
+    "node_modules/@types/d3-random": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz";,
+      "integrity": 
"sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ=="
+    },
     "node_modules/@types/d3-scale": {
       "version": "4.0.4",
       "resolved": 
"https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.4.tgz";,
@@ -4100,6 +4278,16 @@
         "@types/d3-time": "*"
       }
     },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz";,
+      "integrity": 
"sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw=="
+    },
+    "node_modules/@types/d3-selection": {
+      "version": "3.0.6",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.6.tgz";,
+      "integrity": 
"sha512-2ACr96USZVjXR9KMD9IWi1Epo4rSDKnUtYn6q2SPhYxykvXTw9vR77lkFNruXVg4i1tzQtBxeDMx0oNvJWbF1w=="
+    },
     "node_modules/@types/d3-shape": {
       "version": "3.1.2",
       "resolved": 
"https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.2.tgz";,
@@ -4113,11 +4301,33 @@
       "resolved": 
"https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz";,
       "integrity": 
"sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
     },
+    "node_modules/@types/d3-time-format": {
+      "version": "4.0.0",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz";,
+      "integrity": 
"sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw=="
+    },
     "node_modules/@types/d3-timer": {
       "version": "3.0.0",
       "resolved": 
"https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz";,
       "integrity": 
"sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g=="
     },
+    "node_modules/@types/d3-transition": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.4.tgz";,
+      "integrity": 
"sha512-512a4uCOjUzsebydItSXsHrPeQblCVk8IKjqCUmrlvBWkkVh3donTTxmURDo1YPwIVDh5YVwCAO6gR4sgimCPQ==",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.4.tgz";,
+      "integrity": 
"sha512-cqkuY1ah9ZQre2POqjSLcM8g40UVya/qwEUrNYP2/rCVljbmqKCVcv+ebvwhlI5azIbSEL7m+os6n+WlYA43aA==",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
     "node_modules/@types/dagre": {
       "version": "0.7.49",
       "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.49.tgz";,
@@ -4180,6 +4390,11 @@
       "integrity": 
"sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
       "dev": true
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.10",
+      "resolved": 
"https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz";,
+      "integrity": 
"sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
+    },
     "node_modules/@types/graceful-fs": {
       "version": "4.1.6",
       "resolved": 
"https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz";,
@@ -4277,8 +4492,7 @@
     "node_modules/@types/prop-types": {
       "version": "15.7.5",
       "resolved": 
"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz";,
-      "integrity": 
"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
-      "devOptional": true
+      "integrity": 
"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
     },
     "node_modules/@types/qs": {
       "version": "6.9.8",
@@ -4296,7 +4510,6 @@
       "version": "18.2.21",
       "resolved": 
"https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz";,
       "integrity": 
"sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
-      "devOptional": true,
       "dependencies": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -4312,6 +4525,14 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-measure": {
+      "version": "2.0.8",
+      "resolved": 
"https://registry.npmjs.org/@types/react-measure/-/react-measure-2.0.8.tgz";,
+      "integrity": 
"sha512-Pu4/hQ/1AKVN6efoawtcM+l376WYOI8e1fiM6ir4pdLkHilDCkJLjUGvAm0mWKJ0GE6hzu55yCrcJ/xNyEdFwA==",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/resolve": {
       "version": "1.17.1",
       "resolved": 
"https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz";,
@@ -4330,8 +4551,7 @@
     "node_modules/@types/scheduler": {
       "version": "0.16.3",
       "resolved": 
"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz";,
-      "integrity": 
"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
-      "devOptional": true
+      "integrity": 
"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
     },
     "node_modules/@types/semver": {
       "version": "7.5.1",
@@ -6925,8 +7145,47 @@
     "node_modules/csstype": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz";,
-      "integrity": 
"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
-      "devOptional": true
+      "integrity": 
"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+    },
+    "node_modules/d3": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz";,
+      "integrity": 
"sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "dependencies": {
+        "d3-array": "3",
+        "d3-axis": "3",
+        "d3-brush": "3",
+        "d3-chord": "3",
+        "d3-color": "3",
+        "d3-contour": "4",
+        "d3-delaunay": "6",
+        "d3-dispatch": "3",
+        "d3-drag": "3",
+        "d3-dsv": "3",
+        "d3-ease": "3",
+        "d3-fetch": "3",
+        "d3-force": "3",
+        "d3-format": "3",
+        "d3-geo": "3",
+        "d3-hierarchy": "3",
+        "d3-interpolate": "3",
+        "d3-path": "3",
+        "d3-polygon": "3",
+        "d3-quadtree": "3",
+        "d3-random": "3",
+        "d3-scale": "4",
+        "d3-scale-chromatic": "3",
+        "d3-selection": "3",
+        "d3-shape": "3",
+        "d3-time": "3",
+        "d3-time-format": "4",
+        "d3-timer": "3",
+        "d3-transition": "3",
+        "d3-zoom": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
     },
     "node_modules/d3-array": {
       "version": "3.2.4",
@@ -6939,6 +7198,40 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-axis": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz";,
+      "integrity": 
"sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz";,
+      "integrity": 
"sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-chord": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz";,
+      "integrity": 
"sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+      "dependencies": {
+        "d3-path": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-color": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz";,
@@ -6947,6 +7240,88 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-contour": {
+      "version": "4.0.2",
+      "resolved": 
"https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz";,
+      "integrity": 
"sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+      "dependencies": {
+        "d3-array": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": 
"https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz";,
+      "integrity": 
"sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "dependencies": {
+        "delaunator": "5"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay/node_modules/delaunator": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz";,
+      "integrity": 
"sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+      "dependencies": {
+        "robust-predicates": "^3.0.0"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz";,
+      "integrity": 
"sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz";,
+      "integrity": 
"sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz";,
+      "integrity": 
"sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz";,
+      "integrity": 
"sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/d3-ease": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz";,
@@ -6955,6 +7330,30 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz";,
+      "integrity": 
"sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz";,
+      "integrity": 
"sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-format": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz";,
@@ -6963,6 +7362,25 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz";,
+      "integrity": 
"sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "dependencies": {
+        "d3-array": "2.5.0 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": 
"https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz";,
+      "integrity": 
"sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-interpolate": {
       "version": "3.0.1",
       "resolved": 
"https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz";,
@@ -6982,6 +7400,30 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-polygon": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz";,
+      "integrity": 
"sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz";,
+      "integrity": 
"sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz";,
+      "integrity": 
"sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-scale": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz";,
@@ -6997,6 +7439,26 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz";,
+      "integrity": 
"sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz";,
+      "integrity": 
"sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-shape": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz";,
@@ -7038,6 +7500,39 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz";,
+      "integrity": 
"sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz";,
+      "integrity": 
"sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/dagre": {
       "version": "0.8.5",
       "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz";,
@@ -9222,6 +9717,11 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/get-node-dimensions": {
+      "version": "1.2.1",
+      "resolved": 
"https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz";,
+      "integrity": 
"sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ=="
+    },
     "node_modules/get-own-enumerable-property-symbols": {
       "version": "3.0.2",
       "resolved": 
"https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz";,
@@ -9816,7 +10316,6 @@
       "version": "0.6.3",
       "resolved": 
"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz";,
       "integrity": 
"sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
-      "dev": true,
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
@@ -13396,6 +13895,60 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/mobx": {
+      "version": "6.10.2",
+      "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.10.2.tgz";,
+      "integrity": 
"sha512-B1UGC3ieK3boCjnMEcZSwxqRDMdzX65H/8zOHbuTY8ZhvrIjTUoLRR2TP2bPqIgYRfb3+dUigu8yMZufNjn0LQ==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      }
+    },
+    "node_modules/mobx-react": {
+      "version": "7.6.0",
+      "resolved": 
"https://registry.npmjs.org/mobx-react/-/mobx-react-7.6.0.tgz";,
+      "integrity": 
"sha512-+HQUNuh7AoQ9ZnU6c4rvbiVVl+wEkb9WqYsVDzGLng+Dqj1XntHu79PvEWKtSMoMj67vFp/ZPXcElosuJO8ckA==",
+      "dependencies": {
+        "mobx-react-lite": "^3.4.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      },
+      "peerDependencies": {
+        "mobx": "^6.1.0",
+        "react": "^16.8.0 || ^17 || ^18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mobx-react-lite": {
+      "version": "3.4.3",
+      "resolved": 
"https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz";,
+      "integrity": 
"sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mobx";
+      },
+      "peerDependencies": {
+        "mobx": "^6.1.0",
+        "react": "^16.8.0 || ^17 || ^18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/monaco-editor": {
       "version": "0.41.0",
       "resolved": 
"https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.41.0.tgz";,
@@ -14122,6 +14675,24 @@
         "node": ">=4"
       }
     },
+    "node_modules/point-in-svg-path": {
+      "version": "1.0.2",
+      "resolved": 
"https://registry.npmjs.org/point-in-svg-path/-/point-in-svg-path-1.0.2.tgz";,
+      "integrity": 
"sha512-+Smsf7B9e7eRFHIwpN+4rE8inF2APbFWeywPfUgbeh02xdJSkbTz6Pqdt7A36wVCR+CnLbaNkRnBjgOpF5RMVQ==",
+      "engines": {
+        "node": ">=8.x.x"
+      }
+    },
+    "node_modules/popper.js": {
+      "version": "1.16.1",
+      "resolved": 
"https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz";,
+      "integrity": 
"sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
+      "deprecated": "You can find the new Popper v2 at @popperjs/core, this 
package is dedicated to the legacy v1",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs";
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.29",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz";,
@@ -15890,6 +16461,21 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz";,
       "integrity": 
"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "node_modules/react-measure": {
+      "version": "2.5.2",
+      "resolved": 
"https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz";,
+      "integrity": 
"sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==",
+      "dependencies": {
+        "@babel/runtime": "^7.2.0",
+        "get-node-dimensions": "^1.2.1",
+        "prop-types": "^15.6.2",
+        "resize-observer-polyfill": "^1.5.0"
+      },
+      "peerDependencies": {
+        "react": ">0.13.0",
+        "react-dom": ">0.13.0"
+      }
+    },
     "node_modules/react-refresh": {
       "version": "0.11.0",
       "resolved": 
"https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz";,
@@ -16123,8 +16709,7 @@
     "node_modules/regenerator-runtime": {
       "version": "0.14.0",
       "resolved": 
"https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz";,
-      "integrity": 
"sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
-      "dev": true
+      "integrity": 
"sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
     },
     "node_modules/regenerator-transform": {
       "version": "0.15.2",
@@ -16242,6 +16827,11 @@
       "integrity": 
"sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
       "dev": true
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": 
"https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz";,
+      "integrity": 
"sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+    },
     "node_modules/resolve": {
       "version": "1.22.4",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz";,
@@ -16392,6 +16982,11 @@
         "url": "https://github.com/sponsors/isaacs";
       }
     },
+    "node_modules/robust-predicates": {
+      "version": "3.0.2",
+      "resolved": 
"https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz";,
+      "integrity": 
"sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+    },
     "node_modules/rollup": {
       "version": "2.79.1",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz";,
@@ -16490,6 +17085,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz";,
+      "integrity": 
"sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
     "node_modules/rxjs": {
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz";,
@@ -16553,8 +17153,7 @@
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": 
"https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz";,
-      "integrity": 
"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": 
"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/sanitize.css": {
       "version": "13.0.0",
@@ -18548,6 +19147,54 @@
         "minimalistic-assert": "^1.0.0"
       }
     },
+    "node_modules/webcola": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/webcola/-/webcola-3.4.0.tgz";,
+      "integrity": 
"sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==",
+      "dependencies": {
+        "d3-dispatch": "^1.0.3",
+        "d3-drag": "^1.0.4",
+        "d3-shape": "^1.3.5",
+        "d3-timer": "^1.0.5"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-dispatch": {
+      "version": "1.0.6",
+      "resolved": 
"https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz";,
+      "integrity": 
"sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
+    },
+    "node_modules/webcola/node_modules/d3-drag": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz";,
+      "integrity": 
"sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
+      "dependencies": {
+        "d3-dispatch": "1",
+        "d3-selection": "1"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-path": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz";,
+      "integrity": 
"sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+    },
+    "node_modules/webcola/node_modules/d3-selection": {
+      "version": "1.4.2",
+      "resolved": 
"https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz";,
+      "integrity": 
"sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg=="
+    },
+    "node_modules/webcola/node_modules/d3-shape": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz";,
+      "integrity": 
"sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+      "dependencies": {
+        "d3-path": "1"
+      }
+    },
+    "node_modules/webcola/node_modules/d3-timer": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz";,
+      "integrity": 
"sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
+    },
     "node_modules/webidl-conversions": {
       "version": "6.1.0",
       "resolved": 
"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz";,
diff --git a/karavan-web/karavan-app/src/main/webui/package.json 
b/karavan-web/karavan-app/src/main/webui/package.json
index d6d968cc..54e823a3 100644
--- a/karavan-web/karavan-app/src/main/webui/package.json
+++ b/karavan-web/karavan-app/src/main/webui/package.json
@@ -33,6 +33,7 @@
     "@patternfly/react-core": "^5.0.0",
     "@patternfly/react-log-viewer": "^5.0.0",
     "@patternfly/react-table": "^5.0.0",
+    "@patternfly/react-topology": "^5.0.0",
     "@types/js-yaml": "4.0.5",
     "@types/node": "18.16.3",
     "@types/uuid": "9.0.1",
@@ -67,5 +68,8 @@
   "overrides": {
     "@svgr/webpack": "$@svgr/webpack",
     "core-js": "^3.30.1"
+  },
+  "resolutions": {
+    "@types/react": "^18"
   }
 }
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
index cb58a377..00c8b966 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
@@ -24,8 +24,6 @@ import {
     Tabs,
     TabTitleIcon,
     TabTitleText,
-    Tooltip,
-    TooltipPosition,
 } from '@patternfly/react-core';
 import './karavan.css';
 import {RouteDesigner} from "./route/RouteDesigner";
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
index c9d85ccb..d217104a 100644
--- 
a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
+++ 
b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
@@ -23,9 +23,9 @@ import {SagaDefinition} from 
"karavan-core/lib/model/CamelDefinition";
 import {useConnectionsStore, useDesignerStore, useIntegrationStore} from 
"../KaravanStore";
 import {shallow} from "zustand/shallow";
 import {CamelDefinitionApiExt} from 
"karavan-core/lib/api/CamelDefinitionApiExt";
+import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
 
 const overlapGap: number = 40;
-const outgoingDefinitions: string[] = ['ToDefinition', 'KameletDefinition', 
'ToDynamicDefinition', "PollEnrichDefinition", "EnrichDefinition", 
"WireTapDefinition", "SagaDefinition"];
 
 export function DslConnections() {
 
@@ -61,7 +61,7 @@ export function DslConnections() {
     function getIncomings() {
         let outs: [string, number][] = Array.from(steps.values())
             .filter(pos => ["FromDefinition"].includes(pos.step.dslName))
-            .filter(pos => !CamelUi.isElementInternalComponent(pos.step))
+            .filter(pos => !TopologyUtils.isElementInternalComponent(pos.step))
             .filter(pos => !(pos.step.dslName === 'FromDefinition' && 
CamelUi.hasInternalUri(pos.step)))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
@@ -135,10 +135,11 @@ export function DslConnections() {
 
 
     function getOutgoings(): [string, number][] {
+        const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
         let outs: [string, number][] = Array.from(steps.values())
             .filter(pos => outgoingDefinitions.includes(pos.step.dslName))
             .filter(pos => pos.step.dslName !== 'KameletDefinition' || 
(pos.step.dslName === 'KameletDefinition' && 
!CamelUi.isActionKamelet(pos.step)))
-            .filter(pos => pos.step.dslName === 'ToDefinition' && 
!CamelUi.isActionKamelet(pos.step) && 
!CamelUi.isElementInternalComponent(pos.step))
+            .filter(pos => pos.step.dslName === 'ToDefinition' && 
!CamelUi.isActionKamelet(pos.step) && 
!TopologyUtils.isElementInternalComponent(pos.step))
             .filter(pos => !(outgoingDefinitions.includes(pos.step.dslName) && 
CamelUi.hasInternalUri(pos.step)))
             .filter(pos => pos.step.dslName !== 'SagaDefinition')
             .sort((pos1: DslPosition, pos2: DslPosition) => {
@@ -203,6 +204,7 @@ export function DslConnections() {
     }
 
     function getInternals(): [string, number, boolean][] {
+        const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
         let outs: [string, number, boolean][] = Array.from(steps.values())
             .filter(pos => outgoingDefinitions.includes(pos.step.dslName) && 
CamelUi.hasInternalUri(pos.step))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
index c5d63801..1ca65fae 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
@@ -88,6 +88,8 @@ import {
     WorkflowIcon
 } from "./KaravanIcons";
 import React from "react";
+import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
+import {renderToStaticMarkup} from "react-dom/server";
 
 const StepElements: string[] = [
     "AggregateDefinition",
@@ -669,48 +671,23 @@ export class CamelUi {
         }
     }
 
-    static isElementInternalComponent = (element: CamelElement): boolean => {
-        const uri = (element as any).uri;
-        const component = ComponentApi.findByName(uri);
-        return component !== undefined && 
CamelUi.isComponentInternal(component.component.label);
-    }
-
-    static isComponentInternal = (label: string): boolean => {
-        const labels = label.split(",");
-        if (labels.includes('core') && (
-            labels.includes('transformation')
-            || labels.includes('testing')
-            || labels.includes('scheduling')
-            || labels.includes('monitoring')
-            || labels.includes('transformation')
-            || labels.includes('java')
-            || labels.includes('endpoint')
-            || labels.includes('script')
-            || labels.includes('validation')
-        )) {
-            return true;
-        } else if (label === 'transformation') {
-            return true;
-        }
-        return false;
-    }
-
     static getIconForElement = (element: CamelElement): JSX.Element => {
         const uri = (element as any).uri;
         const component = ComponentApi.findByName(uri);
         const k: KameletModel | undefined = CamelUtil.getKamelet(element);
         if (["FromDefinition", "KameletDefinition"].includes(element.dslName) 
&& k !== undefined) {
             return k ? this.getIconFromSource(k.icon()) : 
CamelUi.getIconForDslName(element.dslName);
-        } else if ("FromDefinition" === element.dslName && component !== 
undefined && CamelUi.isComponentInternal(component.component.label)) {
+        } else if ("FromDefinition" === element.dslName && component !== 
undefined && TopologyUtils.isComponentInternal(component.component.label)) {
             return this.getIconForComponent(component?.component.title, 
component?.component.label);
         } else if (element.dslName === "ToDefinition" && (element as 
ToDefinition).uri?.startsWith("kamelet:")) {
             return k ? this.getIconFromSource(k.icon()) : 
CamelUi.getIconForDslName(element.dslName);
-        } else if (element.dslName === "ToDefinition" && component && 
CamelUi.isComponentInternal(component.component.label)) {
+        } else if (element.dslName === "ToDefinition" && component && 
TopologyUtils.isComponentInternal(component.component.label)) {
             return this.getIconForComponent(component?.component.title, 
component?.component.label);
         } else {
             return this.getIconForDslName(element.dslName);
         }
     }
+
     static getIconForDslName = (dslName: string): JSX.Element => {
         switch (dslName) {
             case 'AggregateDefinition':
@@ -761,15 +738,33 @@ export class CamelUi {
         const uri = (element as any).uri;
         const component = ComponentApi.findByName(uri);
         if (component) {
-            return CamelUi.getIconForComponent(component.component.title, 
component.component.label);
+            const reactElement = 
CamelUi.getIconForComponent(component.component.title, 
component.component.label);
+            const icon = 'data:image/svg+xml,' + 
encodeURI(renderToStaticMarkup((reactElement)))
+            return (
+                <svg className="icon">
+                    <image href={icon} className="icon"/>
+                </svg>
+            )
         } else if (["FromDefinition", 
"KameletDefinition"].includes(element.dslName)) {
             const icon = k ? k.icon() : externalIcon;
-            return <img src={icon} className="icon"/>
+            return  (
+                <svg className="icon">
+                    <image href={icon} className="icon"/>
+                </svg>
+            )
         } else if (element.dslName === "ToDefinition" && (element as 
ToDefinition).uri?.startsWith("kamelet:")) {
             const icon = k ? k.icon() : 
CamelUi.getIconSrcForName(element.dslName);
-            return <img src={icon} className="icon"/>
+            return  (
+                <svg className="icon">
+                    <image href={icon} className="icon"/>
+                </svg>
+            )
         } else {
-            return <img src={externalIcon} className="icon"/>;
+            return  (
+                <svg className="icon">
+                    <image href={externalIcon} className="icon"/>
+                </svg>
+            )
         }
     }
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
index c207900a..1f2a623b 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
@@ -60,6 +60,7 @@ export function ProjectPage() {
                     <FlexItem className="project-tabs">
                         {showTabs() && <Tabs activeKey={tab} onSelect={(event, 
tabIndex) => setTab(tabIndex)}>
                             <Tab eventKey="files" title="Files"/>
+                            <Tab eventKey="topology" title="Topology"/>
                             <Tab eventKey="dashboard" title="Dashboard"/>
                             <Tab eventKey="trace" title="Trace"/>
                             <Tab eventKey="build" title="Build"/>
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
index 512530b1..402cf7d1 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
@@ -16,6 +16,7 @@ import {ContainerButtons} from "./container/ContainerButtons";
 import {ProjectContainerTab} from "./container/ProjectContainerTab";
 import {KameletModal} from "../knowledgebase/kamelets/KameletModal";
 import {KameletCard} from "../knowledgebase/kamelets/KameletCard";
+import {TopologyTab} from "./topology/TopologyTab";
 
 export function ProjectPanel() {
 
@@ -38,21 +39,22 @@ export function ProjectPanel() {
     }
 
     const buildIn = isBuildIn();
-    return (
-        <PageSection padding={{default: 'noPadding'}} 
className="scrollable-out">
+    const isTopology = tab === 'topology';
+    return isTopology
+        ? (<TopologyTab/>)
+        : (<PageSection padding={{default: 'noPadding'}} 
className="scrollable-out">
             <PageSection isFilled padding={{default: 'noPadding'}} 
className="scrollable-in">
                 <Flex direction={{default: "column"}} spaceItems={{default: 
"spaceItemsNone"}}>
                     {tab === 'files' && <FlexItem><FilesTab/></FlexItem>}
                     {!buildIn && tab === 'dashboard' && project && 
<FlexItem><DashboardTab/></FlexItem>}
                     {!buildIn && tab === 'trace' && project && 
<FlexItem><TraceTab/></FlexItem>}
                     {!buildIn && tab === 'build' && 
<FlexItem><ProjectBuildTab/></FlexItem>}
-                    {!buildIn && tab === 'build' && config.infrastructure !== 
'kubernetes' && <FlexItem><ImagesPanel/></FlexItem>}
+                    {!buildIn && tab === 'build' && config.infrastructure !== 
'kubernetes' &&
+                        <FlexItem><ImagesPanel/></FlexItem>}
                     {!buildIn && tab === 'container' && 
<FlexItem><ProjectContainerTab/></FlexItem>}
-                    {!buildIn && tab === 'container' && config.infrastructure 
!== 'kubernetes' && <FlexItem><ImagesPanel/></FlexItem>}
+                    {!buildIn && tab === 'container' && config.infrastructure 
!== 'kubernetes' &&
+                        <FlexItem><ImagesPanel/></FlexItem>}
                 </Flex>
             </PageSection>
-        </PageSection>
-
-
-    )
+        </PageSection>)
 }
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/topology/CustomNode.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/CustomNode.tsx
new file mode 100644
index 00000000..40a5995d
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/topology/CustomNode.tsx
@@ -0,0 +1,42 @@
+import * as React from 'react';
+import { RegionsIcon } from '@patternfly/react-icons';
+
+import { DefaultNode, observer} from '@patternfly/react-topology';
+import {getDesignerIcon} from "../../designer/utils/KaravanIcons";
+import {CamelUi} from "../../designer/utils/CamelUi";
+import './topology.css';
+
+function getIcon(data: any) {
+    if (data.icon === 'route') {
+        return (
+            <g transform={`translate(14, 14)`}>
+                {getDesignerIcon('routes')}
+            </g>
+        )
+    } else if (data.icon === 'element') {
+        return (
+            <g transform={`translate(14, 14)`}>
+                {CamelUi.getConnectionIcon(data.step)}
+            </g>
+        )
+    }
+    return <RegionsIcon/>;
+}
+
+const CustomNode: React.FC<any> = observer(({ element, ...rest }) => {
+    const data = element.getData();
+
+    return (
+        <DefaultNode
+            className="common-node"
+            element={element} {...rest}
+            // badge={data.badge}
+            // badgeColor={}
+            // badgeTextColor={badgeColors?.badgeTextColor}
+            // badgeBorderColor={badgeColors?.badgeBorderColor}
+        >
+            {getIcon(data)}
+        </DefaultNode>
+    )
+})
+export default CustomNode;
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyApi.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyApi.tsx
new file mode 100644
index 00000000..3695825e
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyApi.tsx
@@ -0,0 +1,168 @@
+import {
+    ComponentFactory,
+    DefaultEdge,
+    DefaultGroup,
+    EdgeModel,
+    GraphComponent,
+    Model,
+    ModelKind,
+    NodeModel,
+    NodeShape,
+    NodeStatus,
+    withDragNode,
+    withPanZoom
+} from '@patternfly/react-topology';
+import CustomNode from "./CustomNode";
+import {ProjectFile} from "../../api/ProjectModels";
+import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
+import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
+import {TopologyIncomingNode, TopologyOutgoingNode, TopologyRouteNode} from 
"karavan-core/lib/model/TopologyDefinition";
+
+const NODE_DIAMETER = 60;
+
+export function getIntegrations(files: ProjectFile[]): Integration[] {
+    return files.filter((file) => 
file.name.endsWith(".camel.yaml")).map((file) => {
+        return CamelDefinitionYaml.yamlToIntegration(file.name, file.code);
+    })
+}
+
+export function getIncomings(tins: TopologyIncomingNode[]): NodeModel[] {
+    return tins.map(tin => {
+        return {
+            id: tin.id,
+            type: 'node',
+            label: tin.title,
+            width: NODE_DIAMETER,
+            height: NODE_DIAMETER,
+            shape: NodeShape.ellipse,
+            status: NodeStatus.default,
+            data: {
+                isAlternate: false,
+                badge: tin.type,
+                icon: 'element',
+                step: tin.from,
+            }
+        }
+    });
+}
+
+export function getRoutes(tins: TopologyRouteNode[]): NodeModel[] {
+    return tins.map(tin => {
+        const node: NodeModel = {
+            id: tin.id,
+            type: 'node',
+            label: tin.title,
+            width: NODE_DIAMETER,
+            height: NODE_DIAMETER,
+            shape: NodeShape.rect,
+            status: NodeStatus.default,
+            data: {
+                isAlternate: false,
+                icon: 'route'
+            }
+        }
+        return node;
+    });
+}
+
+export function getOutgoings(tons: TopologyOutgoingNode[]): NodeModel[] {
+    return tons.map(tin => {
+        const node: NodeModel = {
+            id: tin.id,
+            type: 'node',
+            label: tin.title,
+            width: NODE_DIAMETER,
+            height: NODE_DIAMETER,
+            shape: NodeShape.ellipse,
+            status: NodeStatus.default,
+            data: {
+                isAlternate: false,
+                icon: 'element',
+                step: tin.step,
+                badge: tin.type
+            }
+        }
+        return node;
+    });
+}
+
+export function getIncomingEdges(tins: TopologyIncomingNode[]): EdgeModel[] {
+    return tins.map(tin => {
+        const node: EdgeModel = {
+            id: 'edge-incoming-' + tin.routeId,
+            type: 'edge',
+            source: tin.id,
+            target: 'route-' + tin.routeId
+        }
+        return node;
+    });
+}
+
+export function getOutgoingEdges(tons: TopologyOutgoingNode[]): EdgeModel[] {
+    return tons.map(tin => {
+        const node: EdgeModel = {
+            id: 'edge-outgoing-' + tin.routeId + '-' + (tin.step as any).id,
+            type: 'edge',
+            source: 'route-' + tin.routeId,
+            target: tin.id
+        }
+        return node;
+    });
+}
+
+export function getModel(files: ProjectFile[]): Model {
+    const integrations = getIntegrations(files);
+    const tins = TopologyUtils.findTopologyIncomingNodes(integrations);
+    const troutes = TopologyUtils.findTopologyRouteNodes(integrations);
+    const tons = TopologyUtils.findTopologyOutgoingNodes(integrations);
+    const trestns = TopologyUtils.findTopologyRestNodes(integrations);
+
+    const nodes: NodeModel[] = [];
+    const groups: NodeModel[] = troutes.map(r => {
+        const children = [r.id]
+        children.push(... tins.filter(i => i.routeId === r.routeId).map(i => 
i.id));
+        children.push(... tons.filter(i => i.routeId === r.routeId).map(i => 
i.id));
+        return   {
+            id: 'group-' + r.routeId,
+            children: children,
+            type: 'group',
+            group: true,
+            label: r.title,
+            style: {
+                padding: 40
+            }
+        }
+    })
+
+    nodes.push(...getIncomings(tins))
+    nodes.push(...getRoutes(troutes))
+    nodes.push(...getOutgoings(tons))
+    nodes.push(...groups)
+    // nodes.push(...groups2)
+
+    const edges: EdgeModel[] = [];
+    edges.push(...getIncomingEdges(tins));
+    edges.push(...getOutgoingEdges(tons));
+
+    return {nodes: nodes, edges: edges, graph: {id: 'g1', type: 'graph', 
layout: 'Dagre'}};
+}
+
+export const customComponentFactory: ComponentFactory = (kind: ModelKind, 
type: string) => {
+    switch (type) {
+        case 'group':
+            return DefaultGroup;
+        default:
+            switch (kind) {
+                case ModelKind.graph:
+                    return withPanZoom()(GraphComponent);
+                case ModelKind.node:
+                    return withDragNode()(CustomNode);
+                // return CustomNode;
+                case ModelKind.edge:
+                    return DefaultEdge;
+                default:
+                    return undefined;
+            }
+    }
+}
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyTab.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyTab.tsx
new file mode 100644
index 00000000..dea2f52a
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyTab.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import {
+    ToolbarItem
+} from '@patternfly/react-core';
+import {
+    action,
+    createTopologyControlButtons,
+    defaultControlButtonsOptions,
+    GRAPH_LAYOUT_END_EVENT,
+    TopologyView,
+    TopologyControlBar,
+    Visualization,
+    VisualizationProvider,
+    VisualizationSurface, DagreLayout, ColaLayout, ForceLayout, 
ColaGroupsLayout, GridLayout,
+} from '@patternfly/react-topology';
+import {customComponentFactory, getModel} from "./TopologyApi";
+import {useFilesStore, useProjectStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+
+export const TopologyTab: React.FC = () => {
+
+    const [files] = useFilesStore((s) => [s.files], shallow);
+    const [project] = useProjectStore((s) => [s.project], shallow);
+
+    const controller = React.useMemo(() => {
+        const model = getModel(files);
+        const newController = new Visualization();
+        newController.registerLayoutFactory((_, graph) => new 
DagreLayout(graph));
+        newController.registerComponentFactory(customComponentFactory);
+
+        newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
+            newController.getGraph().fit(80);
+        });
+
+        newController.fromModel(model, false);
+        return newController;
+    }, []);
+
+    React.useEffect(() => {
+        const model = getModel(files);
+        controller.fromModel(model, false);
+    }, []);
+
+    return (
+        <TopologyView
+            viewToolbar={<ToolbarItem>{}</ToolbarItem>}
+            controlBar={
+                <TopologyControlBar
+                    controlButtons={createTopologyControlButtons({
+                        ...defaultControlButtonsOptions,
+                        zoomInCallback: action(() => {
+                            controller.getGraph().scaleBy(4 / 3);
+                        }),
+                        zoomOutCallback: action(() => {
+                            controller.getGraph().scaleBy(0.75);
+                        }),
+                        fitToScreenCallback: action(() => {
+                            controller.getGraph().fit(80);
+                        }),
+                        resetViewCallback: action(() => {
+                            controller.getGraph().reset();
+                            controller.getGraph().layout();
+                        }),
+                        legend: false
+                    })}
+                />
+            }
+        >
+            <VisualizationProvider controller={controller}>
+                <VisualizationSurface />
+            </VisualizationProvider>
+        </TopologyView>
+    );
+};
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyToolbar.tsx
 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyToolbar.tsx
new file mode 100644
index 00000000..3036c5bb
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/TopologyToolbar.tsx
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, {useEffect, useState} from 'react';
+import {
+    Button,
+    Flex,
+    FlexItem, Form, FormGroup, FormHelperText, Label, Modal, ModalVariant, 
TextInput, Tooltip, TooltipPosition,
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import UploadIcon from "@patternfly/react-icons/dist/esm/icons/upload-icon";
+import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
+import {useFilesStore, useFileStore, useProjectStore} from 
"../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ProjectService} from "../../api/ProjectService";
+import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
+
+export function TopologyToolbar () {
+
+    const [project, isPushing] = useProjectStore((state) => [state.project, 
state.isPushing], shallow )
+    const {files} = useFilesStore();
+    const [file, editAdvancedProperties, setEditAdvancedProperties, 
setAddProperty] = useFileStore((state) =>
+        [state.file, state.editAdvancedProperties, 
state.setEditAdvancedProperties, state.setAddProperty], shallow )
+
+
+    useEffect(() => {
+    }, [project, file]);
+
+    function canAddFiles(): boolean {
+        return !['templates', 'services'].includes(project.projectId);
+    }
+
+    return <Flex className="toolbar" direction={{default: "row"}} 
justifyContent={{default: "justifyContentFlexEnd"}}>
+
+    </Flex>
+}
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/topology/topology.css 
b/karavan-web/karavan-app/src/main/webui/src/project/topology/topology.css
new file mode 100644
index 00000000..f39a1941
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/topology/topology.css
@@ -0,0 +1,4 @@
+.karavan .common-node .icon {
+    height: 32px;
+    width: 32px;
+}
\ No newline at end of file


Reply via email to