UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

219 lines (218 loc) 7.46 kB
/* * Copyright 2025 The Kubernetes Authors * * Licensed 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 ELK from 'elkjs'; import { forEachNode } from './graphModel'; const elk = new ELK({ defaultLayoutOptions: {}, }); const layoutOptions = { nodeSize: { width: 220, height: 70, }, }; const partitionLayers = [ ['Deployment'], ['ReplicaSet', 'ServiceAccount', 'CronJob', 'DaemonSet', 'StatefulSet'], ['Job'], ['Pod', 'RoleBinding'], ['Service', 'NetworkPolicy', 'Role'], ['Endpoints'], ]; /** * To increase readability of the graph we can sort nodes left-to-right * Where more 'owner' nodes like Deployment or ReplicaSet are on the left */ function getPartitionLayer(node) { if (!('kubeObject' in node)) return; const kind = node.kubeObject?.kind; const partitionLayer = partitionLayers.findIndex(layer => layer.includes(kind)); return partitionLayer > -1 ? partitionLayer : undefined; } /** * Prepare the node for the layout by converting it to the ELK node * * @param node - node * @param aspectRatio - aspect ratio of the container */ function convertToElkNode(node, aspectRatio) { const isCollapsed = node.collapsed; const convertedEdges = node.edges ? node.edges .map(edge => { // Make sure source and target exists let hasSource = false; let hasTarget = false; forEachNode(node, n => { if (n.id === edge.source) { hasSource = true; } if (n.id === edge.target) { hasTarget = true; } }); if (!hasSource || !hasTarget) { return; } return { type: 'edge', id: edge.id, sources: [edge.source], targets: [edge.target], label: edge.label, labels: [{ text: edge.label, width: 70, height: 20 }], hidden: false, data: edge.data, }; }) .filter(Boolean) : []; const elkNode = { id: node.id, type: 'object', data: node.data, }; if (node.nodes) { if (node.collapsed) { elkNode.edges = undefined; elkNode.children = undefined; elkNode.width = layoutOptions.nodeSize.width; elkNode.height = layoutOptions.nodeSize.height; return elkNode; } elkNode.layoutOptions = convertedEdges.length > 0 ? { 'partitioning.activate': 'true', 'elk.direction': 'UNDEFINED', // ELK will automatically pick direction 'elk.edgeRouting': 'SPLINES', 'elk.nodeSize.minimum': '(220.0,70.0)', 'elk.nodeSize.constraints': '[MINIMUM_SIZE]', 'elk.algorithm': 'layered', 'elk.spacing.nodeNode': isCollapsed ? '1' : '60', 'elk.layered.spacing.nodeNodeBetweenLayers': '60', 'org.eclipse.elk.stress.desiredEdgeLength': isCollapsed ? '20' : '250', 'org.eclipse.elk.stress.epsilon': '0.1', 'elk.padding': '[left=16, top=16, right=16, bottom=16]', } : { // 'elk.aspectRatio': String(aspectRatio), 'elk.algorithm': 'rectpacking', 'elk.rectpacking.widthApproximation.optimizationGoal': 'ASPECT_RATIO_DRIVEN', 'elk.rectpacking.packing.compaction.rowHeightReevaluation': 'true', 'elk.edgeRouting': 'SPLINES', 'elk.spacing.nodeNode': '20', 'elk.padding': '[left=24, top=24, right=24, bottom=24]', }; elkNode.edges = convertedEdges; elkNode.children = 'collapsed' in node && node.collapsed ? [] : node.nodes.map(node => convertToElkNode(node, aspectRatio)); elkNode.width = layoutOptions.nodeSize.width; elkNode.height = layoutOptions.nodeSize.height; return elkNode; } elkNode.layoutOptions = { 'partitioning.partition': String(getPartitionLayer(node)), }; elkNode.width = layoutOptions.nodeSize.width; elkNode.height = layoutOptions.nodeSize.height; return elkNode; } /** * Convert ELK graph back to react-flow graph */ function convertToReactFlowGraph(elkGraph) { const edges = []; const nodes = []; const pushEdges = (node, parent) => { node.edges?.forEach(edge => { edges.push({ id: edge.id, source: edge.sources[0], target: edge.targets[0], type: edge.type ?? 'customEdge', selectable: false, focusable: false, hidden: false, markerEnd: { type: 'arrowclosed', }, data: { data: edge.data, sections: edge.sections, // @ts-ignore label: edge?.label, labels: edge.labels, parentOffset: { x: (node?.x ?? 0) + (parent?.x ?? 0), y: (node?.y ?? 0) + (parent?.y ?? 0), }, }, }); }); }; const pushNode = (node, parent) => { nodes.push({ id: node.id, type: node.type, style: { width: node.width, height: node.height, }, hidden: false, selectable: true, draggable: false, width: node.width, height: node.height, position: { x: node.x, y: node.y }, data: node.data, parentId: parent?.id ?? undefined, }); }; const convertElkNode = (node, parent) => { pushNode(node, parent); pushEdges(node, parent); node.children?.forEach(it => { convertElkNode(it, node); }); }; pushEdges(elkGraph); elkGraph.children.forEach(node => { convertElkNode(node); }); return { nodes, edges }; } /** * Takes a graph and returns a graph with layout applied * Layout will set size and poisiton for all the elements * * @param graph - root node of the graph * @param aspectRatio - aspect ratio of the container * @returns */ export const applyGraphLayout = (graph, aspectRatio) => { const elkGraph = convertToElkNode(graph, aspectRatio); return elk .layout(elkGraph, { layoutOptions: { 'elk.aspectRatio': String(aspectRatio), }, }) .then(elkGraph => convertToReactFlowGraph(elkGraph)); };