@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
219 lines (218 loc) • 7.46 kB
JavaScript
/*
* 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));
};