mirror of https://github.com/rancher/ui.git
215 lines
6.5 KiB
JavaScript
215 lines
6.5 KiB
JavaScript
/*
|
|
Copyright 2019 Kiali
|
|
|
|
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.
|
|
*/
|
|
|
|
const NAMESPACE_KEY = 'group_compound_layout';
|
|
const CHILDREN_KEY = 'children';
|
|
const STYLES_KEY = 'styles';
|
|
const RELATIVE_POSITION_KEY = `${ NAMESPACE_KEY }relative_position`;
|
|
const COMPOUND_PARENT_NODE_CLASS = '__compoundLayoutParentNodeClass';
|
|
|
|
/**
|
|
* Synthetic edge generator takes care of creating edges without repeating the same edge (targetA -> targetB) twice
|
|
*/
|
|
class SyntheticEdgeGenerator {
|
|
constructor() {
|
|
this.nextId = 0;
|
|
this.generatedMap = {};
|
|
}
|
|
|
|
getEdge(source, target) {
|
|
const sourceId = this.normalizeToParent(source).id();
|
|
const targetId = this.normalizeToParent(target).id();
|
|
const key = `${ sourceId }->${ targetId }`;
|
|
|
|
if (this.generatedMap[key]) {
|
|
return false;
|
|
}
|
|
|
|
this.generatedMap[key] = true;
|
|
|
|
return {
|
|
group: 'edges',
|
|
data: {
|
|
id: `synthetic-edge-${ this.nextId++ }`,
|
|
source: sourceId,
|
|
target: targetId
|
|
}
|
|
};
|
|
}
|
|
|
|
// Returns the parent if any or the element itself.
|
|
normalizeToParent(element) {
|
|
return element.isChild() ? element.parent() : element;
|
|
}
|
|
}
|
|
|
|
export default class GroupCompoundLayout {
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.cy = this.options.cy;
|
|
this.elements = this.options.eles;
|
|
this.syntheticEdgeGenerator = new SyntheticEdgeGenerator();
|
|
}
|
|
|
|
/**
|
|
* This code gets executed on the cy.layout(...).run() is our entrypoint of this algorithm.
|
|
*/
|
|
run() {
|
|
const { realLayout } = this.options;
|
|
const parents = this.parents();
|
|
|
|
const compoundLayoutOptions = {
|
|
fit: false,
|
|
name: 'dagre',
|
|
nodeDimensionsIncludeLabels: true,
|
|
rankDir: 'LR',
|
|
}
|
|
|
|
// (1.a) Prepare parents by assigning a size
|
|
parents.each((parent) => {
|
|
const children = parent.children();
|
|
const targetElements = children.add(children.edgesTo(children));
|
|
|
|
const compoundLayout = targetElements.layout(compoundLayoutOptions);
|
|
|
|
compoundLayout.on('layoutstart layoutready layoutstop', () => {
|
|
return false;
|
|
});
|
|
compoundLayout.run();
|
|
|
|
const boundingBox = targetElements.boundingBox();
|
|
const parentPosition = this.positionFromBoundingBox(boundingBox);
|
|
|
|
parent.children().each((child) => {
|
|
const childPosition = this.positionFromBoundingBox(child.boundingBox());
|
|
const relativePosition = {
|
|
x: childPosition.x - parentPosition.x,
|
|
y: childPosition.y - parentPosition.y
|
|
};
|
|
|
|
child.data(RELATIVE_POSITION_KEY, relativePosition);
|
|
});
|
|
|
|
const backupStyles = {
|
|
shape: parent.style('shape'),
|
|
height: parent.style('height'),
|
|
width: parent.style('width')
|
|
};
|
|
|
|
const newStyles = {
|
|
shape: 'rectangle',
|
|
height: `${ boundingBox.h }px`,
|
|
width: `${ boundingBox.w }px`
|
|
};
|
|
|
|
// Saves a backup of current styles to restore them after we finish
|
|
this.setScratch(parent, STYLES_KEY, backupStyles);
|
|
parent.addClass(COMPOUND_PARENT_NODE_CLASS);
|
|
// (1.b) Set the size
|
|
parent.style(newStyles);
|
|
// Save the children as jsons in the parent scratchpad for later
|
|
this.setScratch(parent, CHILDREN_KEY, parent.children().jsons());
|
|
});
|
|
|
|
// Remove the children and its edges and add synthetic edges for every edge that touches a child node.
|
|
let syntheticEdges = this.cy.collection();
|
|
// Removed elements are being stored because later we will add them back.
|
|
const elementsToRemove = parents.children().reduce((children, child) => {
|
|
children.push(child);
|
|
|
|
return children.concat(
|
|
child.connectedEdges().reduce((edges, edge) => {
|
|
// (1.c) Create synthetic edges.
|
|
const syntheticEdge = this.syntheticEdgeGenerator.getEdge(edge.source(), edge.target());
|
|
|
|
if (syntheticEdge) {
|
|
syntheticEdges = syntheticEdges.add(this.cy.add(syntheticEdge));
|
|
}
|
|
edges.push(edge);
|
|
|
|
return edges;
|
|
}, [])
|
|
);
|
|
}, []);
|
|
|
|
// (1.d) Remove children and edges that touch a child node.
|
|
this.cy.remove(this.cy.collection().add(elementsToRemove));
|
|
|
|
let payload = this.options
|
|
|
|
Object.assign(payload, {
|
|
name: realLayout, // but using the real layout
|
|
eles: this.cy.elements(), // and the current elements
|
|
realLayout: undefined // We don't want this realLayout stuff in there.
|
|
})
|
|
|
|
const layout = this.cy.layout(payload);
|
|
|
|
// (2) Add a one-time callback to be fired when the layout stops
|
|
layout.one('layoutstop', () => {
|
|
// (3) Remove synthetic edges
|
|
this.cy.remove(syntheticEdges);
|
|
|
|
// Add and position the children nodes according to the layout
|
|
parents.each((parent) => {
|
|
// (4.a) Add back the children and the edges
|
|
this.cy.add(this.getScratch(parent, CHILDREN_KEY));
|
|
// (4.b) Layout the children using our compound layout.
|
|
parent.children().each((child) => {
|
|
const relativePosition = child.data(RELATIVE_POSITION_KEY);
|
|
|
|
child.relativePosition(relativePosition);
|
|
child.removeData(RELATIVE_POSITION_KEY);
|
|
});
|
|
|
|
parent.style(this.getScratch(parent, STYLES_KEY));
|
|
parent.removeClass(COMPOUND_PARENT_NODE_CLASS);
|
|
|
|
// Discard the saved values
|
|
this.setScratch(parent, CHILDREN_KEY, undefined);
|
|
this.setScratch(parent, STYLES_KEY, undefined);
|
|
});
|
|
// (4.a) Add the real edges, we already added the children nodes.
|
|
this.cy.add(
|
|
this.cy
|
|
.collection()
|
|
.add(elementsToRemove)
|
|
.edges()
|
|
);
|
|
});
|
|
layout.run();
|
|
}
|
|
|
|
parents() {
|
|
return this.elements.nodes('$node > node');
|
|
}
|
|
|
|
getScratch(element, key) {
|
|
return element.scratch(NAMESPACE_KEY + key);
|
|
}
|
|
|
|
setScratch(element, key, value) {
|
|
element.scratch(NAMESPACE_KEY + key, value);
|
|
}
|
|
|
|
positionFromBoundingBox(boundingBox) {
|
|
return {
|
|
x: boundingBox.x1 + boundingBox.w * 0.5,
|
|
y: boundingBox.y1 + boundingBox.h * 0.5
|
|
};
|
|
}
|
|
}
|