Further adaptations to transformations to align almost identical with how renderings used to be
Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
@@ -33,6 +33,7 @@ import { DefaultRenderNode } from './DefaultRenderNode';
|
||||
import { RelationPairs } from '../../lib/types';
|
||||
import { Direction, EntityEdge, EntityNode } from '../../lib/types';
|
||||
import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges';
|
||||
import { GraphTransformationDebugger } from '../../lib/graph-transformations';
|
||||
|
||||
/** @public */
|
||||
export type EntityRelationsGraphClassKey = 'progress' | 'container' | 'graph';
|
||||
@@ -91,6 +92,7 @@ export type EntityRelationsGraphProps = {
|
||||
renderLabel?: DependencyGraphTypes.RenderLabelFunction<EntityEdge>;
|
||||
curve?: 'curveStepBefore' | 'curveMonotoneX';
|
||||
showArrowHeads?: boolean;
|
||||
onPostTransformation?: GraphTransformationDebugger;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -116,6 +118,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => {
|
||||
renderLabel,
|
||||
curve,
|
||||
showArrowHeads,
|
||||
onPostTransformation,
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
@@ -139,6 +142,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => {
|
||||
entityFilter,
|
||||
onNodeClick,
|
||||
relationPairs,
|
||||
onPostTransformation,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+48
-18
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { MouseEvent, useMemo, useState } from 'react';
|
||||
import { MouseEvent, useState } from 'react';
|
||||
import useDebounce from 'react-use/esm/useDebounce';
|
||||
import { RelationPairs } from '../../lib/types';
|
||||
import { EntityEdge, EntityNode } from '../../lib/types';
|
||||
@@ -22,8 +22,12 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { useRelations } from '../../hooks/useRelations';
|
||||
import { buildGraph } from '../../lib/graph';
|
||||
import {
|
||||
builtInTransformers,
|
||||
TransformerContext,
|
||||
BuiltInTransformations,
|
||||
builtInTransformations,
|
||||
cloneTransformationContext,
|
||||
GraphTransformationDebugger,
|
||||
GraphTransformer,
|
||||
TransformationContext,
|
||||
} from '../../lib/graph-transformations';
|
||||
|
||||
/**
|
||||
@@ -39,6 +43,7 @@ export function useEntityRelationNodesAndEdges({
|
||||
entityFilter,
|
||||
onNodeClick,
|
||||
relationPairs: incomingRelationPairs,
|
||||
onPostTransformation,
|
||||
}: {
|
||||
rootEntityRefs: string[];
|
||||
maxDepth?: number;
|
||||
@@ -49,6 +54,7 @@ export function useEntityRelationNodesAndEdges({
|
||||
entityFilter?: (entity: Entity) => boolean;
|
||||
onNodeClick?: (value: EntityNode, event: MouseEvent<unknown>) => void;
|
||||
relationPairs?: RelationPairs;
|
||||
onPostTransformation?: GraphTransformationDebugger;
|
||||
}): {
|
||||
loading: boolean;
|
||||
nodes?: EntityNode[];
|
||||
@@ -74,11 +80,6 @@ export function useEntityRelationNodesAndEdges({
|
||||
relationPairs: incomingRelationPairs,
|
||||
});
|
||||
|
||||
const forwardRelations = useMemo(
|
||||
() => relationPairs.map(pair => pair[0]),
|
||||
[relationPairs],
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (!entities || Object.keys(entities).length === 0) {
|
||||
@@ -118,32 +119,62 @@ export function useEntityRelationNodesAndEdges({
|
||||
unidirectional,
|
||||
});
|
||||
|
||||
const transformerContext: TransformerContext = {
|
||||
const transformationContext: TransformationContext = {
|
||||
nodeDistances: new Map(),
|
||||
edges,
|
||||
nodes,
|
||||
|
||||
rootEntityRefs,
|
||||
unidirectional,
|
||||
maxDepth,
|
||||
forwardRelations,
|
||||
};
|
||||
builtInTransformers['reduce-edges'](transformerContext);
|
||||
builtInTransformers['set-distances'](transformerContext);
|
||||
|
||||
const debugTransformation = (
|
||||
transformation: BuiltInTransformations | GraphTransformer | undefined,
|
||||
) => {
|
||||
onPostTransformation?.(
|
||||
typeof transformation === 'function'
|
||||
? transformation.name
|
||||
: transformation,
|
||||
transformationContext,
|
||||
cloneTransformationContext(transformationContext),
|
||||
);
|
||||
};
|
||||
|
||||
debugTransformation(undefined);
|
||||
|
||||
const runTransformation = (
|
||||
transformation: BuiltInTransformations | GraphTransformer,
|
||||
) => {
|
||||
if (typeof transformation === 'function') {
|
||||
transformation(transformationContext);
|
||||
} else {
|
||||
builtInTransformations[transformation](transformationContext);
|
||||
}
|
||||
|
||||
debugTransformation(transformation);
|
||||
};
|
||||
|
||||
runTransformation('reduce-edges');
|
||||
runTransformation('set-distances');
|
||||
if (unidirectional) {
|
||||
builtInTransformers['order-forward'](transformerContext);
|
||||
builtInTransformers['strip-distant-edges'](transformerContext);
|
||||
runTransformation('strip-distant-edges');
|
||||
}
|
||||
if (mergeRelations || unidirectional) {
|
||||
// Merge relations even if only unidirectional, the next transformer
|
||||
// 'remove-backward-edges' needs to know about all relations before it
|
||||
// strips away the backward ones
|
||||
builtInTransformers['merge-relations'](transformerContext);
|
||||
runTransformation('merge-relations');
|
||||
}
|
||||
if (unidirectional && !mergeRelations) {
|
||||
builtInTransformers['remove-backward-edges'](transformerContext);
|
||||
runTransformation('order-forward');
|
||||
runTransformation('remove-backward-edges');
|
||||
}
|
||||
|
||||
setNodesAndEdges({ nodes, edges: transformerContext.edges });
|
||||
setNodesAndEdges({
|
||||
nodes: transformationContext.nodes,
|
||||
edges: transformationContext.edges,
|
||||
});
|
||||
},
|
||||
100,
|
||||
[
|
||||
@@ -156,7 +187,6 @@ export function useEntityRelationNodesAndEdges({
|
||||
mergeRelations,
|
||||
onNodeClick,
|
||||
relationPairs,
|
||||
forwardRelations,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { GraphTransformer, TransformerContext } from './types';
|
||||
export type {
|
||||
GraphTransformer,
|
||||
TransformationContext,
|
||||
GraphTransformationDebugger,
|
||||
} from './types';
|
||||
export { cloneTransformationContext } from './types';
|
||||
|
||||
import { reduceEdges } from './reduce-edges';
|
||||
import { setDistances } from './set-distance';
|
||||
@@ -23,7 +28,7 @@ import { stripDistantEdges } from './strip-distant-edges';
|
||||
import { mergeRelations } from './merge-relations';
|
||||
import { removeBackwardEdges } from './remove-backward-edges';
|
||||
|
||||
export const builtInTransformers = {
|
||||
export const builtInTransformations = {
|
||||
'reduce-edges': reduceEdges,
|
||||
'set-distances': setDistances,
|
||||
'order-forward': orderForward,
|
||||
@@ -31,3 +36,5 @@ export const builtInTransformers = {
|
||||
'merge-relations': mergeRelations,
|
||||
'remove-backward-edges': removeBackwardEdges,
|
||||
};
|
||||
|
||||
export type BuiltInTransformations = keyof typeof builtInTransformations;
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
import { EntityEdge } from '../types';
|
||||
import { GraphTransformer } from './types';
|
||||
|
||||
function concatRelations(a: readonly string[], b: readonly string[]): string[] {
|
||||
return Array.from(new Set<string>([...a, ...b]));
|
||||
function concatRelations(
|
||||
a: readonly string[] | undefined,
|
||||
b: readonly string[] | undefined,
|
||||
): string[] {
|
||||
return Array.from(new Set<string>([...(a ?? []), ...(b ?? [])]));
|
||||
}
|
||||
|
||||
/** Merge relations in multiple edges into one edge */
|
||||
export const mergeRelations: GraphTransformer = options => {
|
||||
// The key is on the form `from-to`
|
||||
const mergedEdges = new Map<string, EntityEdge>();
|
||||
|
||||
options.edges.forEach(edge => {
|
||||
const keyForward = `${edge.from}-${edge.to}`;
|
||||
const keyBackward = `${edge.to}-${edge.from}`;
|
||||
@@ -39,12 +42,13 @@ export const mergeRelations: GraphTransformer = options => {
|
||||
);
|
||||
} else if (edgeBackward) {
|
||||
edgeBackward.relations = concatRelations(
|
||||
edge.relations,
|
||||
edgeBackward.relations,
|
||||
edge.relations,
|
||||
);
|
||||
} else {
|
||||
mergedEdges.set(keyForward, edge);
|
||||
}
|
||||
});
|
||||
|
||||
options.edges = Array.from(mergedEdges.values());
|
||||
};
|
||||
|
||||
@@ -14,20 +14,71 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityEdge } from '../types';
|
||||
import { GraphTransformer } from './types';
|
||||
|
||||
/** Orders the edges direction so that the graph goes strictly forward */
|
||||
export const orderForward: GraphTransformer = ({ nodeDistances, edges }) => {
|
||||
export const orderForward: GraphTransformer = ({
|
||||
rootEntityRefs,
|
||||
nodes,
|
||||
edges,
|
||||
}) => {
|
||||
const entityEdges = new Map<string, EntityEdge[]>();
|
||||
edges.forEach(edge => {
|
||||
const fromDistance = nodeDistances.get(edge.from) ?? 0;
|
||||
const toDistance = nodeDistances.get(edge.to) ?? 0;
|
||||
|
||||
if (toDistance < fromDistance) {
|
||||
// Reverse order
|
||||
const { from, to } = edge;
|
||||
edge.from = to;
|
||||
edge.to = from;
|
||||
edge.relations.reverse();
|
||||
let fromEdges = entityEdges.get(edge.from);
|
||||
if (!fromEdges) {
|
||||
fromEdges = [];
|
||||
entityEdges.set(edge.from, fromEdges);
|
||||
}
|
||||
fromEdges.push(edge);
|
||||
|
||||
let toEdges = entityEdges.get(edge.to);
|
||||
if (!toEdges) {
|
||||
toEdges = [];
|
||||
entityEdges.set(edge.to, toEdges);
|
||||
}
|
||||
toEdges.push(edge);
|
||||
});
|
||||
|
||||
const visitedNodes = new Set<string>();
|
||||
|
||||
for (const rootEntityRef of rootEntityRefs) {
|
||||
let currentNodes: string[] = [rootEntityRef].filter(
|
||||
node => !visitedNodes.has(node),
|
||||
);
|
||||
|
||||
while (currentNodes.length > 0) {
|
||||
for (const currentNode of currentNodes) {
|
||||
visitedNodes.add(currentNode);
|
||||
}
|
||||
|
||||
const nextNodes: string[] = [];
|
||||
|
||||
currentNodes.forEach(node => {
|
||||
entityEdges.get(node)?.forEach(edge => {
|
||||
if (edge.to === node && !visitedNodes.has(edge.from)) {
|
||||
// Reverse direction
|
||||
const { from, to } = edge;
|
||||
edge.from = to;
|
||||
edge.to = from;
|
||||
edge.relations.reverse();
|
||||
}
|
||||
|
||||
nextNodes.push(edge.from, edge.to);
|
||||
});
|
||||
});
|
||||
|
||||
currentNodes = Array.from(new Set(nextNodes)).filter(
|
||||
node => !visitedNodes.has(node),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeOrder = Array.from(visitedNodes);
|
||||
|
||||
nodes.sort((a, b) => {
|
||||
const aOrder = nodeOrder.findIndex(node => node === a.id);
|
||||
const bOrder = nodeOrder.findIndex(node => node === b.id);
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { GraphTransformer } from './types';
|
||||
* merged the relations
|
||||
*/
|
||||
export const removeBackwardEdges: GraphTransformer = ctx => {
|
||||
const { forwardRelations, edges } = ctx;
|
||||
const { edges } = ctx;
|
||||
|
||||
const edgesMapFrom = new Map<string, EntityEdge[]>();
|
||||
|
||||
@@ -32,22 +32,9 @@ export const removeBackwardEdges: GraphTransformer = ctx => {
|
||||
edgesMapFrom.set(edge.from, theseEdges);
|
||||
});
|
||||
|
||||
ctx.edges = edges.filter(edge => {
|
||||
if (edge.relations.length === 2) {
|
||||
// If there are two relations, only keep the forward one
|
||||
if (!forwardRelations.includes(edge.relations[1])) {
|
||||
edge.relations.pop();
|
||||
}
|
||||
if (!forwardRelations.includes(edge.relations[0])) {
|
||||
edge.relations.pop();
|
||||
}
|
||||
return edge.relations.length > 0;
|
||||
} else if (edge.relations.length === 1) {
|
||||
// If there is only one relation and it's backwards, remove it
|
||||
if (!forwardRelations.includes(edge.relations[0])) {
|
||||
return false;
|
||||
}
|
||||
edges.forEach(edge => {
|
||||
if (edge.relations.length > 1) {
|
||||
edge.relations = edge.relations.slice(0, 1);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,12 +17,6 @@
|
||||
import { EntityEdge } from '../types';
|
||||
import { GraphTransformer } from './types';
|
||||
|
||||
function minDistance(a: number | undefined, b: number | undefined) {
|
||||
if (typeof a === 'undefined') return b;
|
||||
if (typeof b === 'undefined') return a;
|
||||
return Math.min(a, b);
|
||||
}
|
||||
|
||||
/** Set the distance of each edge to the distance from a root entity */
|
||||
export const setDistances: GraphTransformer = ({
|
||||
nodeDistances,
|
||||
@@ -32,67 +26,45 @@ export const setDistances: GraphTransformer = ({
|
||||
const edgeMap = new Map<string, EntityEdge[]>();
|
||||
edges.forEach(edge => {
|
||||
const fromEdges = edgeMap.get(edge.from) ?? [];
|
||||
edgeMap.set(edge.from, [...fromEdges, edge]);
|
||||
fromEdges.push(edge);
|
||||
edgeMap.set(edge.from, fromEdges);
|
||||
|
||||
const toEdges = edgeMap.get(edge.to) ?? [];
|
||||
edgeMap.set(edge.to, [...toEdges, edge]);
|
||||
toEdges.push(edge);
|
||||
edgeMap.set(edge.to, toEdges);
|
||||
});
|
||||
|
||||
// Sets the distance on as many edges as possible.
|
||||
// Returns true if all edges have a distance
|
||||
const setEdgeDistances = () => {
|
||||
return (
|
||||
edges.filter(edge => {
|
||||
if (
|
||||
rootEntityRefs.includes(edge.from) ||
|
||||
rootEntityRefs.includes(edge.to)
|
||||
) {
|
||||
edge.distance = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
const distance = minDistance(
|
||||
edgeMap
|
||||
.get(edge.from)!
|
||||
.reduce(
|
||||
(prev, cur) => minDistance(prev, cur.distance),
|
||||
undefined as number | undefined,
|
||||
),
|
||||
edgeMap
|
||||
.get(edge.to)!
|
||||
.reduce(
|
||||
(prev, cur) => minDistance(prev, cur.distance),
|
||||
undefined as number | undefined,
|
||||
),
|
||||
);
|
||||
|
||||
if (typeof distance !== 'undefined') {
|
||||
edge.distance = minDistance(edge.distance, distance + 1);
|
||||
}
|
||||
return typeof edge.distance === 'undefined';
|
||||
}).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
let distanceComplete = false;
|
||||
while (!distanceComplete) {
|
||||
distanceComplete = setEdgeDistances();
|
||||
}
|
||||
|
||||
rootEntityRefs.forEach(rootEntityRef => {
|
||||
nodeDistances.set(rootEntityRef, 0);
|
||||
});
|
||||
edges.forEach(edge => {
|
||||
const curFrom = nodeDistances.get(edge.from);
|
||||
const curTo = nodeDistances.get(edge.to);
|
||||
const visitedNodes = new Set<string>(rootEntityRefs);
|
||||
let currentNodes = rootEntityRefs;
|
||||
let distance = 0;
|
||||
|
||||
const fromDistance = minDistance(curFrom, edge.distance);
|
||||
const toDistance = minDistance(curTo, edge.distance);
|
||||
if (typeof fromDistance !== 'undefined') {
|
||||
nodeDistances.set(edge.from, fromDistance);
|
||||
while (currentNodes.length > 0) {
|
||||
const thisDistance = ++distance;
|
||||
|
||||
const nextNodes: string[] = [];
|
||||
currentNodes.forEach(node => {
|
||||
edgeMap.get(node)?.forEach(edge => {
|
||||
edge.distance ??= thisDistance;
|
||||
|
||||
const fromDistance = nodeDistances.get(edge.from) ?? thisDistance;
|
||||
nodeDistances.set(edge.from, fromDistance);
|
||||
nextNodes.push(edge.from);
|
||||
|
||||
const toDistance = nodeDistances.get(edge.to) ?? thisDistance;
|
||||
nodeDistances.set(edge.to, toDistance);
|
||||
nextNodes.push(edge.to);
|
||||
});
|
||||
});
|
||||
|
||||
for (const currentNode of currentNodes) {
|
||||
visitedNodes.add(currentNode);
|
||||
}
|
||||
if (typeof toDistance !== 'undefined') {
|
||||
nodeDistances.set(edge.to, toDistance);
|
||||
}
|
||||
});
|
||||
|
||||
currentNodes = Array.from(new Set(nextNodes)).filter(
|
||||
node => !visitedNodes.has(node),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,23 +14,69 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityEdge } from '../types';
|
||||
import { EntityEdge, EntityNode } from '../types';
|
||||
|
||||
export interface TransformerContext {
|
||||
/**
|
||||
* Contextual information for a graph transformation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TransformationContext {
|
||||
/**
|
||||
* The distance from an entity node to a root entity
|
||||
*
|
||||
* NOTE: This is not set until the setDistances transformation is applied
|
||||
* NOTE: This is empty until the 'set-distances' transformation is applied
|
||||
*/
|
||||
nodeDistances: Map<string, number>;
|
||||
|
||||
edges: EntityEdge[];
|
||||
nodes: EntityNode[];
|
||||
|
||||
// Options:
|
||||
rootEntityRefs: string[];
|
||||
unidirectional: boolean;
|
||||
maxDepth: number;
|
||||
forwardRelations: string[];
|
||||
}
|
||||
|
||||
export type GraphTransformer = (options: TransformerContext) => void;
|
||||
/**
|
||||
* A function that transforms a graph. The function modifies `nodes` and `edges`
|
||||
* in place.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GraphTransformer = (context: TransformationContext) => void;
|
||||
|
||||
/**
|
||||
* A function that debugs a graph transformation.
|
||||
* It is given the three arguments:
|
||||
* * The transformation that was just applied (or undefined before the first
|
||||
* transformation). If the transformation is a function, its `.name` property
|
||||
* will be used.
|
||||
* * The current state (context) of the graph, which _can_ be mutated
|
||||
* * A cloned copy of the context, useful for logging (as transformations are
|
||||
* made in place, the original context is modified and the logs will be
|
||||
* confusing)
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GraphTransformationDebugger = (
|
||||
transformation: string | undefined,
|
||||
transformationContext: TransformationContext,
|
||||
clonedContext: TransformationContext,
|
||||
) => void;
|
||||
|
||||
/** @internal */
|
||||
export function cloneTransformationContext(
|
||||
transformationContext: TransformationContext,
|
||||
): TransformationContext {
|
||||
const clonesContext = JSON.parse(
|
||||
JSON.stringify(transformationContext),
|
||||
) as TransformationContext;
|
||||
clonesContext.edges = clonesContext.edges.sort((a, b) => {
|
||||
return a.from.localeCompare(b.from) || a.to.localeCompare(b.to);
|
||||
});
|
||||
|
||||
clonesContext.nodeDistances = new Map(transformationContext.nodeDistances);
|
||||
|
||||
return clonesContext;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user