mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Merge pull request #6803 from mermaid-js/6774-update-db-class-architecture-diagram
6774: update architecture diagram to reflect new class-based DB structure
This commit is contained in:
5
.changeset/silver-eyes-build.md
Normal file
5
.changeset/silver-eyes-build.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: migrate to class-based ArchitectureDB implementation
|
@@ -1,21 +1,12 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './architectureDb.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
|
||||
const {
|
||||
clear,
|
||||
getDiagramTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
getServices,
|
||||
getGroups,
|
||||
getEdges,
|
||||
getJunctions,
|
||||
} = db;
|
||||
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
describe('architecture diagrams', () => {
|
||||
let db: ArchitectureDB;
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
db = new ArchitectureDB();
|
||||
// @ts-expect-error since type is set to undefined we will have error
|
||||
parser.parser?.yy = db;
|
||||
});
|
||||
|
||||
describe('architecture diagram definitions', () => {
|
||||
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
|
||||
it('should handle title on the first line', async () => {
|
||||
const str = `architecture-beta title Simple Architecture Diagram`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle title on another line', async () => {
|
||||
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
|
||||
title Simple Architecture Diagram
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle accessibility title and description', async () => {
|
||||
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
|
||||
accDescr: Accessibility Description
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccTitle()).toBe('Accessibility Title');
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccTitle()).toBe('Accessibility Title');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
|
||||
it('should handle multiline accessibility description', async () => {
|
||||
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
|
||||
}
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDB,
|
||||
ArchitectureDirectionPair,
|
||||
ArchitectureDirectionPairMap,
|
||||
ArchitectureEdge,
|
||||
@@ -33,330 +33,333 @@ import {
|
||||
isArchitectureService,
|
||||
shiftPositionByArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
|
||||
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
export class ArchitectureDB implements DiagramDB {
|
||||
private nodes: Record<string, ArchitectureNode> = {};
|
||||
private groups: Record<string, ArchitectureGroup> = {};
|
||||
private edges: ArchitectureEdge[] = [];
|
||||
private registeredIds: Record<string, 'node' | 'group'> = {};
|
||||
private dataStructures?: ArchitectureState['dataStructures'];
|
||||
private elements: Record<string, D3Element> = {};
|
||||
|
||||
const state = new ImperativeState<ArchitectureState>(() => ({
|
||||
nodes: {},
|
||||
groups: {},
|
||||
edges: [],
|
||||
registeredIds: {},
|
||||
config: DEFAULT_ARCHITECTURE_CONFIG,
|
||||
dataStructures: undefined,
|
||||
elements: {},
|
||||
}));
|
||||
|
||||
const clear = (): void => {
|
||||
state.reset();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
const addService = function ({
|
||||
id,
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
}: Omit<ArchitectureService, 'edges'>) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'node';
|
||||
public clear(): void {
|
||||
this.nodes = {};
|
||||
this.groups = {};
|
||||
this.edges = [];
|
||||
this.registeredIds = {};
|
||||
this.dataStructures = undefined;
|
||||
this.elements = {};
|
||||
commonClear();
|
||||
}
|
||||
|
||||
state.records.nodes[id] = {
|
||||
public addService({
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getServices = (): ArchitectureService[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
|
||||
|
||||
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
|
||||
state.records.registeredIds[id] = 'node';
|
||||
|
||||
state.records.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getJunctions = (): ArchitectureJunction[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
|
||||
|
||||
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
|
||||
|
||||
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
|
||||
|
||||
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
}: Omit<ArchitectureService, 'edges'>): void {
|
||||
if (this.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
`The service id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'group';
|
||||
|
||||
state.records.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
const getGroups = (): ArchitectureGroup[] => {
|
||||
return Object.values(state.records.groups);
|
||||
};
|
||||
|
||||
const addEdge = function ({
|
||||
lhsId,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
}: ArchitectureEdge<string>) {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`
|
||||
);
|
||||
}
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`
|
||||
);
|
||||
public getServices(): ArchitectureService[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureService);
|
||||
}
|
||||
|
||||
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
const lhsGroupId = state.records.nodes[lhsId].in;
|
||||
const rhsGroupId = state.records.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
public getJunctions(): ArchitectureJunction[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureJunction);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
public getNodes(): ArchitectureNode[] {
|
||||
return Object.values(this.nodes);
|
||||
}
|
||||
|
||||
public getNode(id: string): ArchitectureNode | null {
|
||||
return this.nodes[id] ?? null;
|
||||
}
|
||||
|
||||
public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
|
||||
if (this.registeredIds?.[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'group';
|
||||
|
||||
this.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
public getGroups(): ArchitectureGroup[] {
|
||||
return Object.values(this.groups);
|
||||
}
|
||||
public addEdge({
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
state.records.edges.push(edge);
|
||||
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
|
||||
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const getEdges = (): ArchitectureEdge[] => state.records.edges;
|
||||
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
const getDataStructures = () => {
|
||||
if (state.records.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(state.records.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}: ArchitectureEdge): void {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
|
||||
);
|
||||
}
|
||||
state.records.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (this.nodes[rhsId] === undefined && this.groups[rhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
|
||||
const lhsGroupId = this.nodes[lhsId].in;
|
||||
const rhsGroupId = this.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
rhsDir,
|
||||
rhsInto,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
this.edges.push(edge);
|
||||
if (this.nodes[lhsId] && this.nodes[rhsId]) {
|
||||
this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
}
|
||||
}
|
||||
return state.records.dataStructures;
|
||||
};
|
||||
|
||||
const setElementForId = (id: string, element: D3Element) => {
|
||||
state.records.elements[id] = element;
|
||||
};
|
||||
const getElementById = (id: string) => state.records.elements[id];
|
||||
public getEdges(): ArchitectureEdge[] {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
const getConfig = (): Required<ArchitectureDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
return config;
|
||||
};
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
public getDataStructures() {
|
||||
if (this.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
export const db: ArchitectureDB = {
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
getConfig,
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(this.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = this.getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = this.getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
addService,
|
||||
getServices,
|
||||
addJunction,
|
||||
getJunctions,
|
||||
getNodes,
|
||||
getNode,
|
||||
addGroup,
|
||||
getGroups,
|
||||
addEdge,
|
||||
getEdges,
|
||||
setElementForId,
|
||||
getElementById,
|
||||
getDataStructures,
|
||||
};
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}
|
||||
this.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
};
|
||||
}
|
||||
return this.dataStructures;
|
||||
}
|
||||
|
||||
public setElementForId(id: string, element: D3Element): void {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
|
||||
public getElementById(id: string): D3Element {
|
||||
return this.elements[id];
|
||||
}
|
||||
|
||||
public getConfig(): Required<ArchitectureDiagramConfig> {
|
||||
return cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return this.getConfig()[field];
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
|
||||
* @param field - the config field to access
|
||||
* @returns
|
||||
*/
|
||||
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return getConfig()[field];
|
||||
}
|
||||
// export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
// field: T
|
||||
// ): Required<ArchitectureDiagramConfig>[T] {
|
||||
// return db.getConfig()[field];
|
||||
// }
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
import styles from './architectureStyles.js';
|
||||
import { renderer } from './architectureRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new ArchitectureDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,24 +1,33 @@
|
||||
import type { Architecture } from '@mermaid-js/parser';
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import type { ArchitectureDB } from './architectureTypes.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
|
||||
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
ast.groups.map(db.addGroup);
|
||||
ast.groups.map((group) => db.addGroup(group));
|
||||
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
|
||||
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
|
||||
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
|
||||
ast.edges.map(db.addEdge);
|
||||
ast.edges.map((edge) => db.addEdge(edge));
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parser: {
|
||||
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
|
||||
yy: undefined,
|
||||
},
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Architecture = await parse('architecture', input);
|
||||
log.debug(ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof ArchitectureDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populateDb(ast, db);
|
||||
},
|
||||
};
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import type { Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||
@@ -7,9 +6,10 @@ import { select } from 'd3';
|
||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
NodeSingularData,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
type ArchitectureDB,
|
||||
type ArchitectureDirection,
|
||||
type ArchitectureEdge,
|
||||
type ArchitectureGroup,
|
||||
@@ -44,7 +43,7 @@ registerIconPacks([
|
||||
]);
|
||||
cytoscape.use(fcose);
|
||||
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
services.forEach((service) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
icon: service.icon,
|
||||
label: service.title,
|
||||
parent: service.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-service',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
junctions.forEach((junction) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
type: 'junction',
|
||||
id: junction.id,
|
||||
parent: junction.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-junction',
|
||||
});
|
||||
@@ -257,7 +256,8 @@ function getAlignments(
|
||||
}
|
||||
|
||||
function getRelativeConstraints(
|
||||
spatialMaps: ArchitectureSpatialMap[]
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
db: ArchitectureDB
|
||||
): fcose.FcoseRelativePlacementConstraint[] {
|
||||
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
|
||||
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
|
||||
@@ -296,7 +296,7 @@ function getRelativeConstraints(
|
||||
[ArchitectureDirectionName[
|
||||
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
||||
]]: currId,
|
||||
gap: 1.5 * getConfigField('iconSize'),
|
||||
gap: 1.5 * db.getConfigField('iconSize'),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -353,7 +353,7 @@ function layoutArchitecture(
|
||||
style: {
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'font-size': `${getConfigField('fontSize')}px`,
|
||||
'font-size': `${db.getConfigField('fontSize')}px`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -375,7 +375,7 @@ function layoutArchitecture(
|
||||
selector: '.node-group',
|
||||
style: {
|
||||
// @ts-ignore Incorrect library types
|
||||
padding: `${getConfigField('padding')}px`,
|
||||
padding: `${db.getConfigField('padding')}px`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -393,14 +393,14 @@ function layoutArchitecture(
|
||||
renderEl.remove();
|
||||
|
||||
addGroups(groups, cy);
|
||||
addServices(services, cy);
|
||||
addJunctions(junctions, cy);
|
||||
addServices(services, cy, db);
|
||||
addJunctions(junctions, cy, db);
|
||||
addEdges(edges, cy);
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
|
||||
|
||||
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
|
||||
|
||||
const layout = cy.layout({
|
||||
name: 'fcose',
|
||||
@@ -415,7 +415,9 @@ function layoutArchitecture(
|
||||
const { parent: parentA } = nodeData(nodeA);
|
||||
const { parent: parentB } = nodeData(nodeB);
|
||||
const elasticity =
|
||||
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
|
||||
parentA === parentB
|
||||
? 1.5 * db.getConfigField('iconSize')
|
||||
: 0.5 * db.getConfigField('iconSize');
|
||||
return elasticity;
|
||||
},
|
||||
edgeElasticity(edge: EdgeSingular) {
|
||||
@@ -535,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
|
||||
|
||||
await drawEdges(edgesElem, cy);
|
||||
await drawGroups(groupElem, cy);
|
||||
await drawEdges(edgesElem, cy, db);
|
||||
await drawGroups(groupElem, cy, db);
|
||||
positionNodes(db, cy);
|
||||
|
||||
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
|
||||
setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { db, getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import {
|
||||
ArchitectureDirectionArrow,
|
||||
@@ -16,14 +16,17 @@ import {
|
||||
isArchitectureDirectionY,
|
||||
isArchitecturePairXY,
|
||||
nodeData,
|
||||
type ArchitectureDB,
|
||||
type ArchitectureJunction,
|
||||
type ArchitectureService,
|
||||
} from './architectureTypes.js';
|
||||
|
||||
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
export const drawEdges = async function (
|
||||
edgesEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
const arrowSize = iconSize / 6;
|
||||
const halfArrowSize = arrowSize / 2;
|
||||
@@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core)
|
||||
);
|
||||
};
|
||||
|
||||
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
export const drawGroups = async function (
|
||||
groupsEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const groupIconSize = padding * 0.75;
|
||||
|
||||
const fontSize = getConfigField('fontSize');
|
||||
const fontSize = db.getConfigField('fontSize');
|
||||
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
|
||||
await Promise.all(
|
||||
@@ -266,7 +273,7 @@ export const drawServices = async function (
|
||||
): Promise<number> {
|
||||
for (const service of services) {
|
||||
const serviceElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
if (service.title) {
|
||||
const textElem = serviceElem.append('g');
|
||||
@@ -350,7 +357,7 @@ export const drawJunctions = function (
|
||||
) {
|
||||
junctions.forEach((junction) => {
|
||||
const junctionElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
const bkgElem = junctionElem.append('g');
|
||||
bkgElem
|
||||
|
Reference in New Issue
Block a user