packages/cdk-graph-plugin-diagram/src/internal/graphviz/entities/nodes.ts (132 lines of code) (raw):
/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import { Graph } from "@aws/cdk-graph";
import startCase = require("lodash.startcase"); // eslint-disable-line @typescript-eslint/no-require-imports
import words = require("lodash.words"); // eslint-disable-line @typescript-eslint/no-require-imports
import * as Dot from "ts-graphviz";
import wordWrap = require("word-wrap"); // eslint-disable-line @typescript-eslint/no-require-imports
import { INodePosition } from "../../../config";
import {
resolveCfnResourceImage,
resolveCustomResourceImage,
resolveResourceImage,
} from "../../utils/resource-images";
import { GraphTheme } from "../theme";
/** Diagram label line height */
const LABEL_LINE_HEIGHT = 0.23;
/** Diagram label line max chars */
const LABEL_LINE_MAX_CHARS = 15;
/** Diagram label max number of lines */
const LABEL_MAX_LINES = 5;
/**
* Parsed label structure used for marshalling label rendering
*/
interface MarshalledLabel {
/** Resulting label after transforming */
readonly label: string;
/** Reference to original label before processing */
readonly original: string;
/** Number of lines in the resulting label */
readonly lines: number;
}
/** Marshalls a label to contain length, output multi-line, etc for better rendering */
function marshallLabelForRendering(original: string): MarshalledLabel {
let label = words(original).join(" ");
label = wordWrap(label, {
width: LABEL_LINE_MAX_CHARS,
trim: true,
indent: "",
});
const splitLabel = label.split("\n");
const lines = splitLabel.slice(0, LABEL_MAX_LINES);
// Ellipse last line if dropped lines
if (splitLabel.length > lines.length) {
lines[lines.length - 1] = lines[lines.length - 1] + "...";
}
label = lines
.map((line) => {
line = startCase(line).replace(/ /g, "");
if (line.length > LABEL_LINE_MAX_CHARS) {
return line.substring(0, LABEL_LINE_MAX_CHARS) + "...";
}
return line;
})
.join("\n");
return { original, label, lines: lines.length };
}
/**
* Node class defines a {@link Graph.Node} based diagram {@link Dot.Node}
* @internal
*/
export class Node extends Dot.Node {
/** Reference to the {@link Graph.Node} this diagram {@link Dot.Node} is based on */
readonly graphNode: Graph.Node;
/** Get the label attribute for this node */
get label(): string {
return this.attributes.get("label") as string;
}
set position(pos: INodePosition) {
this.attributes.set("pos", `${pos.x},${pos.y}!`);
}
/** @internal */
constructor(node: Graph.Node) {
super(`node_${node.uuid}`);
this.graphNode = node;
this.attributes.set("label", marshallLabelForRendering(node.id).label);
this.attributes.set(
"comment",
`nodeType:${node.nodeType}` + (node.cfnType ? `(${node.cfnType})` : "")
);
}
}
/**
* ImageNode class extends {@link Node} with support for rendering diagram images.
* @internal
*/
export class ImageNode extends Node {
/** @internal */
constructor(node: Graph.Node, image?: string) {
super(node);
// If image not defined, treat as regular node
if (image) {
this.attributes.apply(GraphTheme.instance.imageNode);
this.attributes.set("image", image);
this.resize();
}
}
/** Get `image` attribute */
get image(): string | undefined {
return this.attributes.get("image") as string | undefined;
}
/** Resizes the node based on image and label dimensions */
resize(baseHeight?: number): void {
if (baseHeight == null) {
baseHeight = (this.attributes.get("height") || 1) as number;
}
const image = this.image;
if (image) {
const labelLines = this.label.split("\n").length;
this.attributes.set("labelloc", "b");
this.attributes.set(
"height",
baseHeight + labelLines * LABEL_LINE_HEIGHT
);
} else {
this.attributes.set("labelloc", "c");
this.attributes.set("penwidth", 0.25);
this.attributes.set("height", baseHeight);
}
}
}
/**
* CfnResourceNode class defines a {@link Dot.Node} based on a {@link Graph.CfnResourceNode}
* @internal
*/
export class CfnResourceNode extends ImageNode {
/** @internal */
constructor(node: Graph.CfnResourceNode) {
super(node, resolveCfnResourceImage(node));
this.attributes.apply(GraphTheme.instance.cfnResourceNode);
this.resize(
GraphTheme.instance.cfnResourceNode.height === ""
? undefined
: GraphTheme.instance.cfnResourceNode.height
);
if (node.isImport) {
this.attributes.apply({
style: "filled,dotted",
penwidth: 1,
fontcolor: (GraphTheme.instance.awsTheme?.text.tertiary ||
"#55555") as Dot.Color,
color: ((GraphTheme.instance.awsTheme?.text.tertiary || "#55555") +
"33") as Dot.Color, // 20%
fillcolor: ((GraphTheme.instance.awsTheme?.text.tertiary || "#55555") +
"1A") as Dot.Color, // 10%
});
}
}
}
/**
* ResourceNode class defines a {@link Dot.Node} based on a {@link Graph.ResourceNode}
* @internal
*/
export class ResourceNode extends ImageNode {
/** @internal */
constructor(node: Graph.ResourceNode) {
const image = resolveResourceImage(node);
super(node, image);
this.attributes.apply(GraphTheme.instance.resourceNode);
this.resize(
GraphTheme.instance.resourceNode.height === ""
? undefined
: GraphTheme.instance.resourceNode.height
);
}
}
/**
* CustomResourceNode class defines a {@link Dot.Node} based on a {@link Graph.Node} for a *custom resource*
* @internal
*/
export class CustomResourceNode extends ImageNode {
/** @internal */
constructor(node: Graph.Node) {
super(node, resolveCustomResourceImage(node));
}
}