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 6e2b259a55637ad13637f901fa1494337c3ab70d Author: Marat Gubaidullin <[email protected]> AuthorDate: Mon Jun 3 17:11:23 2024 -0400 Fix #1309 --- karavan-core/src/core/api/TopologyUtils.ts | 50 +++- karavan-core/src/core/model/TopologyDefinition.ts | 18 +- karavan-core/test/topology.spec.ts | 2 +- karavan-designer/public/example/demo.camel.yaml | 258 ++++++++++++++++++++- .../src/designer/route/DslConnections.tsx | 2 +- karavan-designer/src/topology/CustomNode.tsx | 2 +- karavan-designer/src/topology/TopologyApi.tsx | 84 +++++-- .../src/topology/TopologyPropertiesPanel.tsx | 21 +- karavan-designer/src/topology/TopologyStore.ts | 15 +- karavan-designer/src/topology/TopologyTab.tsx | 14 +- karavan-designer/src/topology/TopologyToolbar.tsx | 22 +- karavan-designer/src/topology/topology.css | 17 +- 12 files changed, 455 insertions(+), 50 deletions(-) diff --git a/karavan-core/src/core/api/TopologyUtils.ts b/karavan-core/src/core/api/TopologyUtils.ts index db76667e..a3bd8a3d 100644 --- a/karavan-core/src/core/api/TopologyUtils.ts +++ b/karavan-core/src/core/api/TopologyUtils.ts @@ -22,7 +22,7 @@ import { PatchDefinition, PostDefinition, PutDefinition, - RestDefinition, SagaDefinition, + RestDefinition, RouteConfigurationDefinition, SagaDefinition, } from '../model/CamelDefinition'; import { CamelElement, @@ -31,7 +31,7 @@ import { import { TopologyIncomingNode, TopologyOutgoingNode, - TopologyRestNode, + TopologyRestNode, TopologyRouteConfigurationNode, TopologyRouteNode, } from '../model/TopologyDefinition'; import { ComponentApi } from './ComponentApi'; @@ -211,7 +211,22 @@ export class TopologyUtils { return result; } - static findTopologyOutgoingNodes = (integrations: Integration[]): TopologyOutgoingNode[] => { + static findTopologyRouteConfigurationNodes = (integration: Integration[]): TopologyRouteConfigurationNode[] => { + const result:TopologyRouteConfigurationNode[] = []; + integration.forEach(i => { + const filename = i.metadata.name; + const routes = i.spec.flows?.filter(flow => flow.dslName === 'RouteConfigurationDefinition'); + const routeElements = routes?.map(r => { + const id = 'route-' + r.id; + const title = '' + (r.description ? r.description : r.id) + return new TopologyRouteConfigurationNode(id, r.id, title, filename, r); + }) || []; + result.push(...routeElements) + }) + return result; + } + + static findTopologyRouteOutgoingNodes = (integrations: Integration[]): TopologyOutgoingNode[] => { const result:TopologyOutgoingNode[] = []; integrations.forEach(i => { const filename = i.metadata.name; @@ -233,6 +248,35 @@ export class TopologyUtils { return result; } + static findTopologyRouteConfigurationOutgoingNodes = (integrations: Integration[]): TopologyOutgoingNode[] => { + const result:TopologyOutgoingNode[] = []; + integrations.forEach(i => { + const filename = i.metadata.name; + const rcs = i.spec.flows?.filter(flow => flow.dslName === 'RouteConfigurationDefinition'); + rcs?.forEach((rc: RouteConfigurationDefinition) => { + const children: CamelElement[] = []; + children.push(...rc.intercept || []); + children.push(...rc.interceptFrom || []); + children.push(...rc.interceptSendToEndpoint || []); + children.push(...rc.onCompletion || []); + children.push(...rc.onException || []); + children.forEach(child => { + const elements = TopologyUtils.findOutgoingInStep(child, []); + elements.forEach((e: any) => { + const id = 'outgoing-' + rc.id + '-' + e.id; + const title = CamelDisplayUtil.getStepDescription(e); + const type = TopologyUtils.isElementInternalComponent(e) ? 'internal' : 'external'; + const connectorType = TopologyUtils.getConnectorType(e); + const uniqueUri = TopologyUtils.getUniqueUri(e); + result.push(new TopologyOutgoingNode(id, type, connectorType, rc.id || 'default', title, filename, e, uniqueUri)); + }) + }) + }) + + }) + return result; + } + static findOutgoingInStep = (step: CamelElement, result: CamelElement[]): CamelElement[] => { if (step !== undefined) { const el = (step as any); diff --git a/karavan-core/src/core/model/TopologyDefinition.ts b/karavan-core/src/core/model/TopologyDefinition.ts index f238f5b9..c4ad5f2f 100644 --- a/karavan-core/src/core/model/TopologyDefinition.ts +++ b/karavan-core/src/core/model/TopologyDefinition.ts @@ -16,7 +16,7 @@ */ import { CamelElement } from './IntegrationDefinition'; -import { FromDefinition, RestDefinition, RouteDefinition } from './CamelDefinition'; +import { FromDefinition, RestDefinition, RouteConfigurationDefinition, RouteDefinition } from './CamelDefinition'; export class TopologyRestNode { path: string; @@ -77,6 +77,22 @@ export class TopologyRouteNode { } } +export class TopologyRouteConfigurationNode { + id: string; + routeConfigurationId: string; + title: string; + fileName: string; + routeConfiguration: RouteConfigurationDefinition + + constructor(id: string, routeConfigurationId: string, title: string, fileName: string, routeConfiguration: RouteConfigurationDefinition) { + this.id = id; + this.routeConfigurationId = routeConfigurationId; + this.title = title; + this.fileName = fileName; + this.routeConfiguration = routeConfiguration; + } +} + export class TopologyOutgoingNode { id: string; type: 'internal' | 'external'; diff --git a/karavan-core/test/topology.spec.ts b/karavan-core/test/topology.spec.ts index db689f21..3f57e19e 100644 --- a/karavan-core/test/topology.spec.ts +++ b/karavan-core/test/topology.spec.ts @@ -29,7 +29,7 @@ describe('Topology functions', () => { 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]); + const ton = TopologyUtils.findTopologyRouteOutgoingNodes([i1, i2]); }); }); diff --git a/karavan-designer/public/example/demo.camel.yaml b/karavan-designer/public/example/demo.camel.yaml index 2c843d16..72b56e9b 100644 --- a/karavan-designer/public/example/demo.camel.yaml +++ b/karavan-designer/public/example/demo.camel.yaml @@ -1,9 +1,257 @@ +- rest: + id: rest-328e + get: + - id: get-5ab7 + to: direct:hello +- beans: + - name: RebateDatabase + type: "#class:org.apache.commons.dbcp2.BasicDataSource" + properties: + username: "{{secret:database-secret/username}}" + password: "{{secret:database-secret/password}}" + url: "{{secret:database-secret/url}}" + driverClassName: org.postgresql.Driver - route: - id: route-d1dc - nodePrefixId: route-0f1 + id: route-0dc7 + description: Audit Start + nodePrefixId: route-972 from: - id: from-852d + id: from-846a + description: Audit Start uri: direct + parameters: + name: start steps: - - loadBalance: - id: loadBalance-d8f0 + - to: + id: to-3597 + uri: kafka + parameters: + topic: audit +- route: + id: route-a54e + description: Audit Finish + nodePrefixId: route-1d1 + from: + id: from-47d5 + description: Audit Finish + uri: direct + parameters: + name: finish + steps: + - to: + id: to-3cf2 + uri: kafka + parameters: + topic: audit +- route: + id: route-07ed + description: Audit Step + nodePrefixId: route-833 + from: + id: from-e007 + uri: direct + parameters: + name: step + steps: + - to: + id: to-fa9e + uri: kafka + parameters: + topic: audit +- route: + id: route-715c + description: Consume Orders + nodePrefixId: route-4f3 + routeConfigurationId: auditedRoute + from: + id: from-7adf + description: From Kafka + uri: kamelet:kafka-scram-source + variableReceive: orderJson + parameters: + bootstrapServers: "{{secret:kafka-secret/bootstrapServers}}" + securityProtocol: "{{secret:kafka-secret/securityProtocol}}" + saslMechanism: "{{secret:kafka-secret/saslMechanism}}" + user: "{{secret:kafka-secret/userName}}" + password: "{{secret:kafka-secret/password}}" + topic: orders + autoCommitEnable: true + autoOffsetReset: earliest + consumerGroup: ordrer-consumer-1 + steps: + - removeHeaders: + id: removeHeaders-c67b + pattern: "*" + - unmarshal: + id: unmarshal-9c0d + variableSend: orderJson + variableReceive: order + json: + id: json-0b83 + - choice: + id: choice-f872 + when: + - id: when-c007 + description: Special Orders + expression: + groovy: + id: groovy-15e2 + expression: variables.order.type == 'SPECIAL' + steps: + - log: + id: log-735e + message: "SPECIAL: ${variable.order}" + - to: + id: to-7a02 + uri: direct + parameters: + name: special + otherwise: + id: otherwise-d9a2 + steps: + - log: + id: log-f600 + message: "ORDINAL: ${variable.order}" + - to: + id: to-c45b + uri: direct + parameters: + name: ordinal +- route: + id: route-1695 + description: Enrich Ordinal Order + nodePrefixId: route-f1b + from: + id: from-471d + uri: direct + parameters: + name: ordinal + steps: + - log: + id: log-202b + message: ${body} + - to: + id: to-f1b3 + uri: direct + parameters: + name: send +- route: + id: route-9a71 + description: Send to HTTP + nodePrefixId: route-fdf + from: + id: from-b83d + uri: direct + parameters: + name: send + steps: + - log: + id: log-ca7f + message: "send: ${variable.order}" + - marshal: + id: marshal-a102 + variableSend: order + variableReceive: orderOut + json: + id: json-d9be + - setHeaders: + id: setHeaders-2a6c + headers: + - id: setHeader-7dd5 + name: X-API-Key + expression: + simple: + id: simple-e2a5 + expression: "{{secret:http-secret/X-API-Key}}" + - to: + id: to-fb94 + description: Send to HTTP Service + variableSend: orderOut + uri: http + parameters: + httpUri: http-to-database.talisman/order + httpMethod: POST +- route: + id: route-e9e4 + description: Enrich Special Order + nodePrefixId: route-3ca + from: + id: from-b883 + uri: direct + parameters: + name: special + steps: + - to: + id: to-6000 + description: Get Rebate + variableReceive: rebate + uri: sql + parameters: + dataSource: "#bean:RebateDatabase" + query: SELECT * FROM REBATES WHERE NAME = 'SPECIAL' + outputType: SelectOne + - log: + id: log-8558 + message: ${variable.rebate} + - setVariable: + id: setVariable-d9a6 + description: Update Order + name: order + expression: + groovy: + id: groovy-a753 + expression: >- + def updatedOrder = variables.order; + + updatedOrder.amount = updatedOrder.amount * (1 - + variables.rebate.rebate); + + return updatedOrder; + - to: + id: to-20bb + uri: direct + parameters: + name: send +- route: + id: hello + from: + id: from-3f49 + uri: direct + parameters: + name: hello + steps: + - to: + id: to-428d + uri: activemq + - to: + id: to-fed7 + uri: kafka +- routeConfiguration: + id: auditedRoute + intercept: + - intercept: + id: intercept-0deb + steps: + - to: + id: to-b470 + uri: direct + parameters: + name: step + interceptFrom: + - interceptFrom: + id: interceptFrom-4041 + steps: + - to: + id: to-6861 + uri: direct + parameters: + name: start + onCompletion: + - onCompletion: + id: onCompletion-3dab + steps: + - to: + id: to-dd4e + uri: direct + parameters: + name: finish diff --git a/karavan-designer/src/designer/route/DslConnections.tsx b/karavan-designer/src/designer/route/DslConnections.tsx index 718f60be..f0b0ff7a 100644 --- a/karavan-designer/src/designer/route/DslConnections.tsx +++ b/karavan-designer/src/designer/route/DslConnections.tsx @@ -46,7 +46,7 @@ export function DslConnections() { const integrations = getIntegrations(files); setTons(prevState => { const data = new Map<string, string[]>(); - TopologyUtils.findTopologyOutgoingNodes(integrations).forEach(t => { + TopologyUtils.findTopologyRouteOutgoingNodes(integrations).forEach(t => { const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters?.name; if (data.has(key)) { const list = data.get(key) || []; diff --git a/karavan-designer/src/topology/CustomNode.tsx b/karavan-designer/src/topology/CustomNode.tsx index d683d04f..88263966 100644 --- a/karavan-designer/src/topology/CustomNode.tsx +++ b/karavan-designer/src/topology/CustomNode.tsx @@ -24,7 +24,7 @@ import {CamelUi} from "../designer/utils/CamelUi"; import './topology.css'; function getIcon(data: any) { - if (['route', 'rest'].includes(data.icon)) { + if (['route', 'rest', 'routeConfiguration'].includes(data.icon)) { return ( <g transform={`translate(14, 14)`}> {getDesignerIcon(data.icon)} diff --git a/karavan-designer/src/topology/TopologyApi.tsx b/karavan-designer/src/topology/TopologyApi.tsx index a640d508..d24b38f8 100644 --- a/karavan-designer/src/topology/TopologyApi.tsx +++ b/karavan-designer/src/topology/TopologyApi.tsx @@ -26,20 +26,21 @@ import { NodeModel, NodeShape, NodeStatus, - withPanZoom, withSelection + withPanZoom, + withSelection } from '@patternfly/react-topology'; import CustomNode from "./CustomNode"; -import {Integration} from "karavan-core/lib/model/IntegrationDefinition"; +import {Integration, IntegrationFile} 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, TopologyRestNode, + TopologyRouteConfigurationNode, TopologyRouteNode } from "karavan-core/lib/model/TopologyDefinition"; import CustomEdge from "./CustomEdge"; -import {IntegrationFile} from "karavan-core/lib/model/IntegrationDefinition"; import CustomGroup from "./CustomGroup"; const NODE_DIAMETER = 60; @@ -94,6 +95,28 @@ export function getRoutes(tins: TopologyRouteNode[]): NodeModel[] { return node; }); } +export function getRouteConfigurations(trcs: TopologyRouteConfigurationNode[]): NodeModel[] { + return trcs.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, + type: 'routeConfiguration', + icon: 'routeConfiguration', + step: tin.routeConfiguration, + routeConfigurationId: tin.routeConfigurationId, + fileName: tin.fileName, + } + } + return node; + }); +} export function getOutgoingNodes(tons: TopologyOutgoingNode[]): NodeModel[] { return tons.filter(tin => tin.type === 'external').map(tin => { @@ -227,41 +250,64 @@ export function getInternalEdges(tons: TopologyOutgoingNode[], tins: TopologyInc return result; } -export function getModel(files: IntegrationFile[]): Model { +export function getModel(files: IntegrationFile[], grouping?: boolean): Model { const integrations = getIntegrations(files); const tins = TopologyUtils.findTopologyIncomingNodes(integrations); const troutes = TopologyUtils.findTopologyRouteNodes(integrations); - const tons = TopologyUtils.findTopologyOutgoingNodes(integrations); + const tons = TopologyUtils.findTopologyRouteOutgoingNodes(integrations); const trestns = TopologyUtils.findTopologyRestNodes(integrations); + const trcs = TopologyUtils.findTopologyRouteConfigurationNodes(integrations); + const trcons = TopologyUtils.findTopologyRouteConfigurationOutgoingNodes(integrations); + const nodes: NodeModel[] = []; - const groups: NodeModel[] = troutes.map(r => { - const children = [r.id] - children.push(...tins.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); - children.push(...tons.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); - return { - id: 'group-' + r.routeId, - children: children, - type: 'group', - group: true, - label: r.title, - style: { - padding: 40 - } + const groups: NodeModel[] = []; + + const children1 = [] + children1.push(...tins.filter(i => i.type === 'external').map(i => i.id)); + children1.push(...trestns.map(i => i.id)); + groups.push({ + id: 'consumer-group', + children: children1, + type: 'group', + group: true, + label: 'Consumer group', + style: { + padding: 10, + strokeWidth: "2px", + } + }) + + const children2 = [...tons.filter(i => i.type === 'external').map(i => i.id)]; + groups.push({ + id: 'producer-group', + children: children2, + type: 'group', + group: true, + label: 'Producer group', + style: { + padding: 10, + strokeWidth: "2px" } }) nodes.push(...getRestNodes(trestns)) nodes.push(...getIncomingNodes(tins)) nodes.push(...getRoutes(troutes)) + nodes.push(...getRouteConfigurations(trcs)) nodes.push(...getOutgoingNodes(tons)) - // nodes.push(...groups) + nodes.push(...getOutgoingNodes(trcons)) + + if (grouping === true) { + nodes.push(...groups) + } const edges: EdgeModel[] = []; edges.push(...getIncomingEdges(tins)); edges.push(...getOutgoingEdges(tons)); edges.push(...getRestEdges(trestns, tins)); edges.push(...getInternalEdges(tons, tins)); + edges.push(...getInternalEdges(trcons, tins)); edges.push(...getExternalEdges(tons,tins)); return {nodes: nodes, edges: edges, graph: {id: 'g1', type: 'graph', layout: 'Dagre'}}; diff --git a/karavan-designer/src/topology/TopologyPropertiesPanel.tsx b/karavan-designer/src/topology/TopologyPropertiesPanel.tsx index 27ef71b5..a32d6969 100644 --- a/karavan-designer/src/topology/TopologyPropertiesPanel.tsx +++ b/karavan-designer/src/topology/TopologyPropertiesPanel.tsx @@ -48,6 +48,14 @@ export function TopologyPropertiesPanel(props: Props) { return false; } + function isRouteConfiguration() { + return (nodeData && nodeData.type === 'routeConfiguration'); + } + + function isRest() { + return (nodeData && nodeData.type === 'rest'); + } + function isKamelet() { if (nodeData && nodeData.type === 'step') { const uri: string = nodeData?.step?.uri || ''; @@ -70,7 +78,16 @@ export function TopologyPropertiesPanel(props: Props) { } function getTitle () { - return isRoute() ? "Route" : (isKamelet() ? "Kamelet" : "Component"); + if (isRoute()) { + return "Route"; + } else if (isKamelet()) { + return "Kamelet"; + } else if (isRouteConfiguration()) { + return "Route Configuration"; + } else if (isRest()) { + return "REST"; + } + return "Component"; } function getHeader() { @@ -118,7 +135,7 @@ export function TopologyPropertiesPanel(props: Props) { return ( <TopologySideBar className="topology-sidebar" - show={selectedIds.length > 0} + show={selectedIds.length > 0 && nodeData} header={getHeader()} > <DslProperties designerType={'routes'}/> diff --git a/karavan-designer/src/topology/TopologyStore.ts b/karavan-designer/src/topology/TopologyStore.ts index 517e2794..514f1b37 100644 --- a/karavan-designer/src/topology/TopologyStore.ts +++ b/karavan-designer/src/topology/TopologyStore.ts @@ -28,6 +28,8 @@ interface TopologyState { setRanker: (ranker: string) => void nodeData: any setNodeData: (nodeData: any) => void + showGroups?: boolean + setShowGroups: (showGroups: boolean) => void } export const useTopologyStore = createWithEqualityFn<TopologyState>((set) => ({ @@ -50,8 +52,13 @@ export const useTopologyStore = createWithEqualityFn<TopologyState>((set) => ({ }, nodeData: undefined, setNodeData: (nodeData: any) => { - set((state: TopologyState) => { - return {nodeData: nodeData}; - }); -}, + set((state: TopologyState) => { + return {nodeData: nodeData}; + }); + }, + setShowGroups: (showGroups: boolean) => { + set((state: TopologyState) => { + return {showGroups: showGroups}; + }); + }, }), shallow) diff --git a/karavan-designer/src/topology/TopologyTab.tsx b/karavan-designer/src/topology/TopologyTab.tsx index e9587444..cc189e29 100644 --- a/karavan-designer/src/topology/TopologyTab.tsx +++ b/karavan-designer/src/topology/TopologyTab.tsx @@ -49,8 +49,8 @@ interface Props { export function TopologyTab(props: Props) { - const [selectedIds, setSelectedIds, setFileName, ranker, setRanker, setNodeData] = useTopologyStore((s) => - [s.selectedIds, s.setSelectedIds, s.setFileName, s.ranker, s.setRanker, s.setNodeData], shallow); + const [selectedIds, setSelectedIds, setFileName, ranker, setRanker, setNodeData, showGroups] = useTopologyStore((s) => + [s.selectedIds, s.setSelectedIds, s.setFileName, s.ranker, s.setRanker, s.setNodeData, s.showGroups], shallow); const [setSelectedStep] = useDesignerStore((s) => [s.setSelectedStep], shallow) function setTopologySelected(model: Model, ids: string []) { @@ -60,8 +60,8 @@ export function TopologyTab(props: Props) { if (node && node.length > 0) { const data = node[0].data; setNodeData(data); - setFileName(data.fileName) - if (data.step) { + if (data && data.step) { + setFileName(data.fileName) setSelectedStep(data.step) } else { setSelectedStep(undefined); @@ -72,7 +72,7 @@ export function TopologyTab(props: Props) { } const controller = React.useMemo(() => { - const model = getModel(props.files); + const model = getModel(props.files, showGroups); const newController = new Visualization(); newController.registerLayoutFactory((_, graph) => new DagreLayout(graph, { @@ -99,9 +99,9 @@ export function TopologyTab(props: Props) { React.useEffect(() => { setSelectedIds([]) - const model = getModel(props.files); + const model = getModel(props.files, showGroups); controller.fromModel(model, false); - }, [ranker, controller, setSelectedIds, props.files]); + }, [ranker, controller, setSelectedIds, props.files, showGroups]); const controlButtons = React.useMemo(() => { // const customButtons = [ diff --git a/karavan-designer/src/topology/TopologyToolbar.tsx b/karavan-designer/src/topology/TopologyToolbar.tsx index 5dbe4949..0de61fee 100644 --- a/karavan-designer/src/topology/TopologyToolbar.tsx +++ b/karavan-designer/src/topology/TopologyToolbar.tsx @@ -17,10 +17,12 @@ import * as React from 'react'; import { - Button, ToolbarContent, + Button, Switch, ToolbarContent, ToolbarItem, Tooltip } from '@patternfly/react-core'; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; +import {useTopologyStore} from "./TopologyStore"; +import {shallow} from "zustand/shallow"; interface Props { onClickAddRoute: () => void @@ -31,8 +33,22 @@ interface Props { export function TopologyToolbar (props: Props) { + const [showGroups, setShowGroups] = useTopologyStore((s) => + [s.showGroups, s.setShowGroups], shallow); + return ( - <ToolbarContent> + <div className='topology-toolbar'> + <ToolbarItem className="group-switch"> + <Tooltip content={"Show Consumer and Producer Groups"} position={"bottom-start"}> + <Switch + id="reversed-switch" + label="Groups" + isChecked={showGroups} + onChange={(_, checked) => setShowGroups(checked)} + isReversed + /> + </Tooltip> + </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Integration Route"} position={"bottom"}> <Button className="dev-action-button" size="sm" @@ -77,6 +93,6 @@ export function TopologyToolbar (props: Props) { </Button> </Tooltip> </ToolbarItem> - </ToolbarContent> + </div> ) } \ No newline at end of file diff --git a/karavan-designer/src/topology/topology.css b/karavan-designer/src/topology/topology.css index 24fe7679..7d21c7b2 100644 --- a/karavan-designer/src/topology/topology.css +++ b/karavan-designer/src/topology/topology.css @@ -15,17 +15,28 @@ * limitations under the License. */ +.karavan .topology-panel .topology-toolbar { + width: 100%; + display: flex; + flex-direction: row; +} + +.karavan .topology-panel .topology-toolbar .group-switch { + flex-grow: 4; + margin-top: auto; + margin-bottom: auto; + margin-left: 6px; +} + .karavan .topology-panel .pf-v5-c-toolbar { padding: 0; - display: flex; - flex-direction: column; - align-items: flex-end; height: fit-content; row-gap: 0; } .karavan .topology-panel .pf-v5-c-toolbar .pf-v5-c-toolbar__content { padding: 0 6px 0 0; + width: 100%; } .karavan .topology-panel .pf-v5-c-toolbar .pf-v5-c-toolbar__content-section {
