@peter.prib/renderengine 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -213
- package/REFERENCE.md +272 -6
- package/dist/renderengine-workspace.umd.js +11980 -7421
- package/dist/renderengine.umd.js +8991 -5784
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +534 -0
- package/package.json +70 -68
- package/src/bian.js +182 -0
- package/src/charting/CheckboxTreeSelector.js +373 -304
- package/src/charting/DiagramEditor.js +53 -32
- package/src/charting/DiagramTypeRegistry.js +13 -3
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
- package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/DataModel.js +645 -0
- package/src/charting/datamodel/DataModelChecker.js +92 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +108 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +129 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +51 -0
- package/src/charting/datamodel/LevelMapping.js +38 -0
- package/src/charting/datamodel/LevelSchema.js +41 -0
- package/src/charting/datamodel/ServiceDomain.js +177 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +15 -0
- package/src/charting/diagramModel/MergeModelCommand.js +43 -37
- package/src/charting/diagramModel/OpenViewList.js +75 -0
- package/src/charting/diagramModel/ViewSetChecker.js +22 -34
- package/src/charting/diagrams/DataModelDiagram.js +160 -86
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +169 -158
- package/src/charting/diagrams/nesting.js +31 -0
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/modelTree/ModelTree.js +202 -164
- package/src/charting/modelTree/ModelTreePanel.js +36 -3
- package/src/charting/modelTree/TreeViewMode.js +131 -110
- package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
- package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
- package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
- package/src/datamodel.js +132 -122
- package/src/diagramCatalog.js +35 -30
- package/src/index.js +2666 -2659
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/index.js +1 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
- package/src/workspace/parts/ChartPart.js +6 -1
- package/src/workspace/parts/DataModelPart.js +544 -0
- package/src/workspace/parts/DiagramEditorPart.js +2 -8
- package/src/workspace/parts/EditorPart.js +461 -407
- package/src/workspace/parts/ModelFile.js +216 -187
- package/src/workspace/parts/ModelsPart.js +483 -192
- package/src/workspace/storage/BackupFolder.js +145 -0
- package/src/workspace/storage/KeyValueStore.js +92 -0
- package/src/workspace/storage/ModelLibrary.js +65 -0
- package/src/workspace/workArea.js +417 -413
- package/src/yaml/YamlReader.js +372 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LevelMapping — which entities of one level realise which of another
|
|
3
|
+
* (conceptual → logical, logical → physical): pairs of ids, any number either
|
|
4
|
+
* way, so one concept may become several logical entities (Party → Person,
|
|
5
|
+
* Organisation), several one table (Person, Organisation → party), or many
|
|
6
|
+
* many.
|
|
7
|
+
*/
|
|
8
|
+
export class LevelMapping {
|
|
9
|
+
/**
|
|
10
|
+
* @param {{from: string, to: string, pairs?: Array<[string, string]>}} spec
|
|
11
|
+
* `from`/`to` levels; each pair [id at `from`, id at `to`]
|
|
12
|
+
*/
|
|
13
|
+
constructor({ from, to, pairs = [] } = {}) {
|
|
14
|
+
if (!from || !to) throw new TypeError('LevelMapping needs the levels it maps, from and to');
|
|
15
|
+
this.from = from;
|
|
16
|
+
this.to = to;
|
|
17
|
+
this.pairs = pairs.map(([a, b]) => [a, b]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What realises an entity of `from` at `to`. */
|
|
21
|
+
targetsOf(id) {
|
|
22
|
+
return this.pairs.filter(([a]) => a === id).map(([, b]) => b);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What an entity of `to` realises at `from`. */
|
|
26
|
+
sourcesOf(id) {
|
|
27
|
+
return this.pairs.filter(([, b]) => b === id).map(([a]) => a);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Map one more pair (none twice). */
|
|
31
|
+
add(fromId, toId) {
|
|
32
|
+
if (!this.pairs.some(([a, b]) => a === fromId && b === toId)) this.pairs.push([fromId, toId]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toJSON() {
|
|
36
|
+
return { from: this.from, to: this.to, pairs: this.pairs.map(p => [...p]) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LevelSchema — one level of a data model (conceptual, logical, physical) as
|
|
3
|
+
* its own structure: its entities (a concept; a logical entity with its
|
|
4
|
+
* attributes and keys; a table with its columns and types) and the
|
|
5
|
+
* relationships between them. The levels are not one-to-one: a concept may be
|
|
6
|
+
* realised by several logical entities, several by one table (LevelMapping
|
|
7
|
+
* says which).
|
|
8
|
+
*
|
|
9
|
+
* An entity: `{ id, name, description?, attributes?, x?, y?, color?, shape?,
|
|
10
|
+
* labelPosition? }` — `attributes` `[{name, type?, isPK?, isFK?}]` (none at the
|
|
11
|
+
* conceptual level); a relationship as dataModelDiagram takes it.
|
|
12
|
+
*/
|
|
13
|
+
export class LevelSchema {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} level - 'conceptual', 'logical' or 'physical'
|
|
16
|
+
* @param {{entities?: Array<Object>, relationships?: Array<Object>}} [spec]
|
|
17
|
+
*/
|
|
18
|
+
constructor(level, { entities = [], relationships = [] } = {}) {
|
|
19
|
+
if (!level) throw new TypeError('LevelSchema needs its level');
|
|
20
|
+
this.level = level;
|
|
21
|
+
this.entities = entities;
|
|
22
|
+
this.relationships = relationships;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An entity by id, or null. */
|
|
26
|
+
entity(id) {
|
|
27
|
+
return this.entities.find(e => e.id === id) ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Add an entity (its id unique at this level). */
|
|
31
|
+
addEntity(entity) {
|
|
32
|
+
if (!entity?.id) throw new TypeError('An entity needs an id');
|
|
33
|
+
if (this.entity(entity.id)) throw new TypeError(`The ${this.level} level already has "${entity.id}"`);
|
|
34
|
+
this.entities.push(entity);
|
|
35
|
+
return entity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toJSON() {
|
|
39
|
+
return { entities: this.entities, relationships: this.relationships };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { DataModelPerspective } from './DataModelPerspective.js';
|
|
2
|
+
import { BianNotation } from '../../bian.js';
|
|
3
|
+
|
|
4
|
+
/** How a service domain's control record and behaviour qualifiers are coloured where it is seen through. */
|
|
5
|
+
const CONTROL_RECORD_COLOR = '#fde68a';
|
|
6
|
+
const QUALIFIER_COLOR = '#e0f2fe';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ServiceDomain — a BIAN service domain as a kind of perspective: it sees the
|
|
10
|
+
* concepts it covers (as any perspective), and it is more. It sits in a business
|
|
11
|
+
* area and business domain, manages one kind of asset (`asset`) with one
|
|
12
|
+
* functional pattern (`pattern`, BianNotation), and keeps a control record (a
|
|
13
|
+
* logical entity: what its instances are, named for the asset and the
|
|
14
|
+
* pattern's generic artifact) broken down by behaviour qualifiers (each a
|
|
15
|
+
* logical entity, of a qualifier type). Its service operations (its semantic
|
|
16
|
+
* API) follow: the pattern's action terms on the control record and each
|
|
17
|
+
* qualifier (BianNotation.serviceOperations).
|
|
18
|
+
*
|
|
19
|
+
* Seen through at the logical level it shows its control record and qualifiers,
|
|
20
|
+
* whatever they realise, marked (colour, and what each is on hover), with a note
|
|
21
|
+
* of its service operations on the control record.
|
|
22
|
+
*/
|
|
23
|
+
export class ServiceDomain extends DataModelPerspective {
|
|
24
|
+
static kind = 'service-domain';
|
|
25
|
+
static kindLabel = 'Service domain';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} spec - As DataModelPerspective, plus:
|
|
29
|
+
* @param {string} [spec.businessArea]
|
|
30
|
+
* @param {string} [spec.businessDomain]
|
|
31
|
+
* @param {string} [spec.asset] - What it manages ('Current Account'); its name when unset
|
|
32
|
+
* @param {string} [spec.pattern] - A BIAN functional pattern ('Fulfill')
|
|
33
|
+
* @param {string|null} [spec.controlRecord] - The logical entity its control record is
|
|
34
|
+
* @param {Array<{id?: string, name: string, type?: string, entity?: string}>} [spec.qualifiers]
|
|
35
|
+
* Behaviour qualifiers: a name, a qualifier type (default the pattern's), the logical entity it is
|
|
36
|
+
* @param {string[]} [spec.owns] - Other business objects (logical entities) it owns
|
|
37
|
+
*/
|
|
38
|
+
constructor({ businessArea = '', businessDomain = '', asset = '', pattern = 'Fulfill', controlRecord = null, qualifiers = [], owns = [], ...spec } = {}) {
|
|
39
|
+
super(spec);
|
|
40
|
+
/** Business objects (logical entities) it owns besides its control record and qualifiers. */
|
|
41
|
+
this.owns = [...owns];
|
|
42
|
+
this.businessArea = businessArea;
|
|
43
|
+
this.businessDomain = businessDomain;
|
|
44
|
+
this.asset = asset;
|
|
45
|
+
this.pattern = pattern;
|
|
46
|
+
this.controlRecord = controlRecord;
|
|
47
|
+
this.qualifiers = qualifiers.map((q, i) => ({ ...q, id: q.id ?? `${this.id}-bq-${i + 1}` }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Its functional pattern (BianFunctionalPattern), or null for one BIAN does not have. */
|
|
51
|
+
get functionalPattern() {
|
|
52
|
+
return BianNotation.functionalPattern(this.pattern);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Its control record's name: the asset (else its name) and the pattern's generic artifact. */
|
|
56
|
+
get controlRecordName() {
|
|
57
|
+
return this.functionalPattern?.controlRecordName(this.asset || this.name) ?? (this.asset || this.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A qualifier's type: its own, else its pattern's. */
|
|
61
|
+
qualifierType(qualifier) {
|
|
62
|
+
return qualifier.type || this.functionalPattern?.qualifierType || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Its default service operations (BianNotation.serviceOperations). */
|
|
66
|
+
serviceOperations() {
|
|
67
|
+
return BianNotation.serviceOperations(this.pattern, {
|
|
68
|
+
serviceDomain: this.name, controlRecord: this.controlRecordName, qualifiers: this.qualifiers.map(q => q.name),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The business objects it owns: its control record, its qualifiers' entities, and those it names (`owns`). */
|
|
73
|
+
ownedEntities() {
|
|
74
|
+
return [...new Set([this.controlRecord, ...this.qualifiers.map(q => q.entity), ...this.owns].filter(Boolean))];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** At the logical level, the business objects it owns. */
|
|
78
|
+
alsoShows(level) {
|
|
79
|
+
return level === 'logical' ? this.ownedEntities() : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** At the logical level, its control record and qualifiers are coloured and say what they are. */
|
|
83
|
+
decorate(entity, level) {
|
|
84
|
+
if (level !== 'logical') return {};
|
|
85
|
+
if (entity.id === this.controlRecord) {
|
|
86
|
+
return { color: CONTROL_RECORD_COLOR, details: { 'control record': this.controlRecordName, 'functional pattern': this.pattern, 'service domain': this.name } };
|
|
87
|
+
}
|
|
88
|
+
const qualifier = this.qualifiers.find(q => q.entity === entity.id);
|
|
89
|
+
return qualifier
|
|
90
|
+
? { color: QUALIFIER_COLOR, details: { 'behaviour qualifier': `${qualifier.name} (${this.qualifierType(qualifier)})`, 'service domain': this.name } }
|
|
91
|
+
: {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** At the logical level, a note of its service operations, on its control record. */
|
|
95
|
+
notes(level) {
|
|
96
|
+
if (level !== 'logical' || !this.controlRecord) return [];
|
|
97
|
+
return [{ id: `${this.id}-operations`, text: this.operationsSummary(), attachTo: [this.controlRecord] }];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Its service operations in a few lines: the terms, on what. */
|
|
101
|
+
operationsSummary() {
|
|
102
|
+
const ops = this.serviceOperations();
|
|
103
|
+
const terms = (pick) => [...new Set(ops.filter(pick).map(o => o.actionTerm))].join(', ');
|
|
104
|
+
const qualifiers = this.qualifiers.map(q => q.name);
|
|
105
|
+
return [
|
|
106
|
+
`${this.name}: ${terms(o => o.target === this.name)}`,
|
|
107
|
+
`${this.controlRecordName}: ${terms(o => o.target === this.controlRecordName && !o.qualifier)}`,
|
|
108
|
+
...(qualifiers.length ? [`each of ${qualifiers.join(', ')}: the same`] : []),
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
toJSON() {
|
|
113
|
+
return {
|
|
114
|
+
...super.toJSON(),
|
|
115
|
+
businessArea: this.businessArea, businessDomain: this.businessDomain, asset: this.asset, pattern: this.pattern,
|
|
116
|
+
controlRecord: this.controlRecord, qualifiers: this.qualifiers.map(q => ({ ...q })), owns: [...this.owns],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Its dialog's fields: a perspective's, then where it sits, its asset and
|
|
122
|
+
* pattern (BIAN's, each described), its control record (a logical entity),
|
|
123
|
+
* and its qualifiers a line each: `name | type | logical entity`.
|
|
124
|
+
*/
|
|
125
|
+
static fields(model, domain = null) {
|
|
126
|
+
const logical = model.level('logical').entities;
|
|
127
|
+
const entityName = (id) => logical.find(e => e.id === id)?.name ?? id ?? '';
|
|
128
|
+
return [
|
|
129
|
+
...DataModelPerspective.fields(model, domain),
|
|
130
|
+
{ type: 'heading', label: 'Service domain' },
|
|
131
|
+
{ key: 'businessArea', label: 'Business area', type: 'text', value: domain?.businessArea ?? '' },
|
|
132
|
+
{ key: 'businessDomain', label: 'Business domain', type: 'text', value: domain?.businessDomain ?? '' },
|
|
133
|
+
{ key: 'asset', label: 'Asset it manages', type: 'text', value: domain?.asset ?? '', placeholder: 'its name' },
|
|
134
|
+
{
|
|
135
|
+
key: 'pattern', label: 'Functional pattern', type: 'select', value: domain?.pattern ?? 'Fulfill',
|
|
136
|
+
choices: BianNotation.functionalPatterns().map(p => [p.name, `${p.name} — ${p.artifact}`]),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: 'controlRecord', label: 'Control record', type: 'select', value: domain?.controlRecord ?? '',
|
|
140
|
+
choices: [['', 'None yet'], ...logical.map(e => [e.id, e.name])],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'qualifiers', label: 'Behaviour qualifiers', type: 'textarea', rows: 4,
|
|
144
|
+
value: (domain?.qualifiers ?? []).map(q => [q.name, q.type ?? '', entityName(q.entity)].join(' | ')).join('\n'),
|
|
145
|
+
hint: `One a line: name | type (${BianNotation.qualifierTypes().slice(0, 4).join(', ')}…; blank: the pattern's) | logical entity (by name)`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: 'owns', label: 'Also owns', type: 'checklist', value: domain?.owns ?? [],
|
|
149
|
+
choices: logical.map(e => [e.id, e.name]),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A service domain's spec from those fields (a qualifier kept keeps its id; a type BIAN lacks is left to the pattern). */
|
|
155
|
+
static specFrom(values, model, domain = null) {
|
|
156
|
+
const logical = model.level('logical').entities;
|
|
157
|
+
const entityOf = (word) => logical.find(e => e.id === word || e.name.toLowerCase() === word.toLowerCase())?.id ?? null;
|
|
158
|
+
const qualifiers = String(values.qualifiers ?? '').split('\n').map(l => l.trim()).filter(Boolean).map(line => {
|
|
159
|
+
const [name, type = '', entity = ''] = line.split('|').map(p => p.trim());
|
|
160
|
+
const kept = domain?.qualifiers.find(q => q.name === name);
|
|
161
|
+
return {
|
|
162
|
+
...(kept && { id: kept.id }), name,
|
|
163
|
+
...(BianNotation.isQualifierType(type) && { type }),
|
|
164
|
+
...(entityOf(entity) && { entity: entityOf(entity) }),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
kind: ServiceDomain.kind,
|
|
169
|
+
...DataModelPerspective.specFrom(values, model),
|
|
170
|
+
businessArea: (values.businessArea ?? '').trim(), businessDomain: (values.businessDomain ?? '').trim(),
|
|
171
|
+
asset: (values.asset ?? '').trim(), pattern: values.pattern || 'Fulfill',
|
|
172
|
+
controlRecord: values.controlRecord || null, qualifiers, owns: [...(values.owns ?? [])],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
DataModelPerspective.register(ServiceDomain);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { BianNotation } from '../../bian.js';
|
|
2
|
+
|
|
3
|
+
/** The colour of a service domain's box, by its functional pattern's place among BIAN's (none: grey). */
|
|
4
|
+
const PATTERN_COLORS = ['#fde68a', '#bbf7d0', '#bfdbfe', '#fbcfe8', '#ddd6fe', '#fed7aa', '#a5f3fc', '#e9d5ff', '#fecaca', '#d9f99d'];
|
|
5
|
+
const NO_PATTERN_COLOR = '#e5e7eb';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ServiceLandscape — a model's service domains laid out as BIAN draws its
|
|
9
|
+
* service landscape: a column per business area, in it a frame per business
|
|
10
|
+
* domain, in that the service domains (each a box coloured by its functional
|
|
11
|
+
* pattern, and on hover its pattern, control record and asset). Service
|
|
12
|
+
* domains with no area or domain go under "No business area" / "No business
|
|
13
|
+
* domain". The layout follows from the structure, so it is made anew each time.
|
|
14
|
+
*
|
|
15
|
+
* `view(serviceDomains)` gives what dataModelDiagram draws: entities (a box per
|
|
16
|
+
* service domain, its id `landscape:<id>`), and the frames as graphDiagram
|
|
17
|
+
* folders (`folders`, `elementFolders`).
|
|
18
|
+
*/
|
|
19
|
+
export class ServiceLandscape {
|
|
20
|
+
static BOX = Object.freeze({ width: 160, height: 56 });
|
|
21
|
+
static GAP = 16;
|
|
22
|
+
static HEADER = 24;
|
|
23
|
+
static PAD = 12;
|
|
24
|
+
/** Service domains side by side in a business domain's frame. */
|
|
25
|
+
static ACROSS = 2;
|
|
26
|
+
static NO_AREA = 'No business area';
|
|
27
|
+
static NO_DOMAIN = 'No business domain';
|
|
28
|
+
static PREFIX = 'landscape:';
|
|
29
|
+
|
|
30
|
+
/** A service domain's id from a landscape box's id, or null. */
|
|
31
|
+
static serviceDomainOf(nodeId) {
|
|
32
|
+
return String(nodeId).startsWith(ServiceLandscape.PREFIX) ? String(nodeId).slice(ServiceLandscape.PREFIX.length) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The colour of a functional pattern's service domains. */
|
|
36
|
+
static colorOf(pattern) {
|
|
37
|
+
const i = BianNotation.functionalPatterns().findIndex(p => p.name === pattern);
|
|
38
|
+
return i < 0 ? NO_PATTERN_COLOR : PATTERN_COLORS[i % PATTERN_COLORS.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Business areas, each with its business domains, each with its service
|
|
43
|
+
* domains, in the order they first come.
|
|
44
|
+
* @param {Array<ServiceDomain>} serviceDomains
|
|
45
|
+
* @returns {Array<{name: string, domains: Array<{name: string, serviceDomains: Array}>}>}
|
|
46
|
+
*/
|
|
47
|
+
static areas(serviceDomains) {
|
|
48
|
+
const areas = new Map();
|
|
49
|
+
for (const sd of serviceDomains) {
|
|
50
|
+
const area = sd.businessArea || ServiceLandscape.NO_AREA;
|
|
51
|
+
const domain = sd.businessDomain || ServiceLandscape.NO_DOMAIN;
|
|
52
|
+
if (!areas.has(area)) areas.set(area, new Map());
|
|
53
|
+
const domains = areas.get(area);
|
|
54
|
+
if (!domains.has(domain)) domains.set(domain, []);
|
|
55
|
+
domains.get(domain).push(sd);
|
|
56
|
+
}
|
|
57
|
+
return [...areas].map(([name, domains]) => ({ name, domains: [...domains].map(([d, list]) => ({ name: d, serviceDomains: list })) }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What to draw (see the class).
|
|
62
|
+
* @param {Array<ServiceDomain>} serviceDomains
|
|
63
|
+
* @returns {{entities, relationships, annotations, links, folders, elementFolders}}
|
|
64
|
+
*/
|
|
65
|
+
static view(serviceDomains) {
|
|
66
|
+
const { BOX, GAP, HEADER, PAD, ACROSS, PREFIX } = ServiceLandscape;
|
|
67
|
+
const domainWidth = ACROSS * BOX.width + (ACROSS - 1) * GAP + 2 * PAD;
|
|
68
|
+
const areaWidth = domainWidth + 2 * PAD;
|
|
69
|
+
const entities = [], areaFolders = [], domainFolders = [], elementFolders = [];
|
|
70
|
+
ServiceLandscape.areas(serviceDomains).forEach((area, a) => {
|
|
71
|
+
const areaId = `landscape-area-${a + 1}`;
|
|
72
|
+
const x = 20 + a * (areaWidth + GAP);
|
|
73
|
+
let y = 20 + HEADER + PAD;
|
|
74
|
+
area.domains.forEach((domain, d) => {
|
|
75
|
+
const domainId = `${areaId}-domain-${d + 1}`;
|
|
76
|
+
const rows = Math.ceil(domain.serviceDomains.length / ACROSS);
|
|
77
|
+
const height = HEADER + PAD + rows * BOX.height + (rows - 1) * GAP + PAD;
|
|
78
|
+
domainFolders.push({ id: domainId, name: domain.name, parentId: areaId, x: x + PAD, y, width: domainWidth, height });
|
|
79
|
+
domain.serviceDomains.forEach((sd, i) => {
|
|
80
|
+
const id = `${PREFIX}${sd.id}`;
|
|
81
|
+
const pattern = sd.functionalPattern;
|
|
82
|
+
entities.push({
|
|
83
|
+
id, name: sd.name, level: 'landscape',
|
|
84
|
+
x: x + 2 * PAD + (i % ACROSS) * (BOX.width + GAP) + BOX.width / 2,
|
|
85
|
+
y: y + HEADER + PAD + Math.floor(i / ACROSS) * (BOX.height + GAP) + BOX.height / 2,
|
|
86
|
+
width: BOX.width, height: BOX.height,
|
|
87
|
+
color: ServiceLandscape.colorOf(sd.pattern),
|
|
88
|
+
description: sd.description,
|
|
89
|
+
details: {
|
|
90
|
+
'functional pattern': pattern ? `${pattern.name} (${pattern.artifact})` : (sd.pattern || 'none'),
|
|
91
|
+
'control record': sd.controlRecordName,
|
|
92
|
+
...(sd.asset && { asset: sd.asset }),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
elementFolders.push([id, domainId]);
|
|
96
|
+
});
|
|
97
|
+
y += height + GAP;
|
|
98
|
+
});
|
|
99
|
+
areaFolders.push({ id: areaId, name: area.name, x, y: 20, width: areaWidth, height: y - 20 - GAP + PAD });
|
|
100
|
+
});
|
|
101
|
+
// Areas first, so they are drawn behind their domains.
|
|
102
|
+
return { entities, relationships: [], annotations: [], links: [], folders: [...areaFolders, ...domainFolders], elementFolders };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DataType } from './DataType.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StructuredType — a data type made of named fields, each of a type (a plain
|
|
5
|
+
* type's name or another data type's): BIAN's Amount, Datetime, Identifier.
|
|
6
|
+
* `fields` as `[{ name, type?, description?, iso20022? }]`. Drawn as a box with
|
|
7
|
+
* a row per field, joined to the data types its fields are.
|
|
8
|
+
*/
|
|
9
|
+
export class StructuredType extends DataType {
|
|
10
|
+
static kind = 'structure';
|
|
11
|
+
static kindLabel = 'Structure';
|
|
12
|
+
|
|
13
|
+
/** @param {Object} spec - As DataType, plus `fields: [{ name, type?, description?, iso20022? }]` */
|
|
14
|
+
constructor({ fields = [], ...spec } = {}) {
|
|
15
|
+
super(spec);
|
|
16
|
+
this.fields = fields.map(f => ({
|
|
17
|
+
name: String(f.name),
|
|
18
|
+
...(f.type && { type: String(f.type) }),
|
|
19
|
+
...(f.description && { description: f.description }),
|
|
20
|
+
...(f.iso20022 && { iso20022: f.iso20022 }),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
summary() {
|
|
25
|
+
return `${this.name}: ${this.fields.map(f => (f.type ? `${f.name} (${f.type})` : f.name)).join(', ')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rows() {
|
|
29
|
+
return this.fields.map(f => ({ name: f.name, ...(f.type && { type: f.type }) }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
uses() {
|
|
33
|
+
return [...super.uses(), ...this.fields.map(f => f.type).filter(Boolean)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toJSON() {
|
|
37
|
+
return { ...super.toJSON(), fields: this.fields.map(f => ({ ...f })) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A plain type's fields, then its fields a line each: `name | type`. */
|
|
41
|
+
static fields(model, type = null) {
|
|
42
|
+
return [
|
|
43
|
+
...DataType.fields(model, type),
|
|
44
|
+
{
|
|
45
|
+
key: 'fields', label: 'Fields', type: 'textarea', rows: 6,
|
|
46
|
+
value: (type?.fields ?? []).map(f => (f.type ? `${f.name} | ${f.type}` : f.name)).join('\n'),
|
|
47
|
+
hint: 'One a line: name | type (a plain type, or a data type by name)',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static specFrom(values, model, type = null) {
|
|
53
|
+
return {
|
|
54
|
+
...super.specFrom(values),
|
|
55
|
+
fields: DataType.lines(values.fields).map(([name, fieldType]) => {
|
|
56
|
+
const kept = type?.fields.find(f => f.name === name);
|
|
57
|
+
return { ...kept, name, ...(fieldType ? { type: fieldType } : { type: undefined }) };
|
|
58
|
+
}).map(f => JSON.parse(JSON.stringify(f))),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
DataType.register(StructuredType);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { DataModel } from './DataModel.js';
|
|
2
|
+
export { DataModelPerspective } from './DataModelPerspective.js';
|
|
3
|
+
export { DataModelContext } from './DataModelContext.js';
|
|
4
|
+
export { ServiceDomain } from './ServiceDomain.js';
|
|
5
|
+
export { DataModelChecker } from './DataModelChecker.js';
|
|
6
|
+
export { DataType } from './DataType.js';
|
|
7
|
+
export { EnumerationType } from './EnumerationType.js';
|
|
8
|
+
export { StructuredType } from './StructuredType.js';
|
|
9
|
+
export { ServiceLandscape } from './ServiceLandscape.js';
|
|
10
|
+
export { BianLandscapeReader } from './BianLandscapeReader.js';
|
|
11
|
+
export { BianSemanticApiReader } from './BianSemanticApiReader.js';
|
|
12
|
+
export { LevelSchema } from './LevelSchema.js';
|
|
13
|
+
export { LevelMapping } from './LevelMapping.js';
|
|
14
|
+
export { DataModelFileFormat } from './DataModelFileFormat.js';
|
|
15
|
+
export { LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared } from './DataModelCompared.js';
|
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import { Command } from '../CommandHistory.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MergeModelCommand — things from another model brought into this one
|
|
5
|
-
* (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
|
|
6
|
-
* order given (what is needed first). One undo step; undo takes each back,
|
|
7
|
-
* last first.
|
|
8
|
-
*/
|
|
9
|
-
export class MergeModelCommand extends Command {
|
|
10
|
-
/** A merge is no edit on one view: undoing it shows no other. */
|
|
11
|
-
viewId = null;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {ViewSetModel} model - Merged into
|
|
15
|
-
* @param {ViewSetModel} from - Merged from
|
|
16
|
-
* @param {Array<{kind: ComparedKind, item: Object}>} steps
|
|
17
|
-
* @param {string} [label]
|
|
18
|
-
*/
|
|
19
|
-
constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
|
|
20
|
-
super(label);
|
|
21
|
-
this.model = model;
|
|
22
|
-
this.from = from;
|
|
23
|
-
this.steps = steps;
|
|
24
|
-
this._undos = [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
execute() {
|
|
28
|
-
this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
|
|
29
|
-
this.
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
undo() {
|
|
33
|
-
for (const undo of [...this._undos].reverse()) undo();
|
|
34
|
-
this._undos = [];
|
|
35
|
-
this.
|
|
36
|
-
}
|
|
37
|
-
|
|
1
|
+
import { Command } from '../CommandHistory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MergeModelCommand — things from another model brought into this one
|
|
5
|
+
* (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
|
|
6
|
+
* order given (what is needed first). One undo step; undo takes each back,
|
|
7
|
+
* last first.
|
|
8
|
+
*/
|
|
9
|
+
export class MergeModelCommand extends Command {
|
|
10
|
+
/** A merge is no edit on one view: undoing it shows no other. */
|
|
11
|
+
viewId = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ViewSetModel} model - Merged into
|
|
15
|
+
* @param {ViewSetModel} from - Merged from
|
|
16
|
+
* @param {Array<{kind: ComparedKind, item: Object}>} steps
|
|
17
|
+
* @param {string} [label]
|
|
18
|
+
*/
|
|
19
|
+
constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
|
|
20
|
+
super(label);
|
|
21
|
+
this.model = model;
|
|
22
|
+
this.from = from;
|
|
23
|
+
this.steps = steps;
|
|
24
|
+
this._undos = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
execute() {
|
|
28
|
+
this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
|
|
29
|
+
this._changed();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
undo() {
|
|
33
|
+
for (const undo of [...this._undos].reverse()) undo();
|
|
34
|
+
this._undos = [];
|
|
35
|
+
this._changed();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The model changed: a view set tells its listeners; a model without views (a DataModel) says it was edited. @private */
|
|
39
|
+
_changed() {
|
|
40
|
+
this.model._notify?.();
|
|
41
|
+
this.model.edited?.();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenViewList — the views of a model that have a tab, in tab order, as a
|
|
3
|
+
* browser keeps its tabs: closing a tab leaves the view in the model, and a
|
|
4
|
+
* view is opened by going to it (from the tree, a search, a new view). It
|
|
5
|
+
* starts with the view in front only, so a model opened or imported with many
|
|
6
|
+
* views does not fill the tab strip.
|
|
7
|
+
*
|
|
8
|
+
* It follows the model: `sync()` drops views since deleted and opens the one in
|
|
9
|
+
* front, unless paused (`while`: a report passing through every view opens none).
|
|
10
|
+
*/
|
|
11
|
+
export class OpenViewList {
|
|
12
|
+
/**
|
|
13
|
+
* @param {ViewSetModel} model
|
|
14
|
+
* @param {Iterable<string>} [ids] - Those open to start with (default: the view in front)
|
|
15
|
+
*/
|
|
16
|
+
constructor(model, ids = null) {
|
|
17
|
+
this.model = model;
|
|
18
|
+
this.ids = ids ? [...ids] : (model.currentViewId ? [model.currentViewId] : []);
|
|
19
|
+
this._paused = 0;
|
|
20
|
+
this.sync();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Follow the model: deleted views gone, the one in front open (last). Returns the open views. */
|
|
24
|
+
sync() {
|
|
25
|
+
this.ids = this.ids.filter(id => this.model.getView(id));
|
|
26
|
+
const current = this.model.currentViewId;
|
|
27
|
+
if (!this._paused && current && !this.ids.includes(current)) this.ids.push(current);
|
|
28
|
+
return this.views();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The open views, in tab order. */
|
|
32
|
+
views() {
|
|
33
|
+
return this.ids.map(id => this.model.getView(id)).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isOpen(id) {
|
|
37
|
+
return this.ids.includes(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Open a view's tab (if not open) and bring it to the front. */
|
|
41
|
+
open(id) {
|
|
42
|
+
if (!this.model.getView(id)) return false;
|
|
43
|
+
if (!this.ids.includes(id)) this.ids.push(id);
|
|
44
|
+
if (this.model.currentViewId !== id) this.model.switchTo(id);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether a tab may be closed: one always stays open. */
|
|
49
|
+
canClose(id) {
|
|
50
|
+
return this.ids.includes(id) && this.ids.length > 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Close a view's tab; the view stays in the model. Closing the tab in front
|
|
55
|
+
* brings its neighbour (the next, else the one before) to the front.
|
|
56
|
+
* @returns {boolean} whether it closed
|
|
57
|
+
*/
|
|
58
|
+
close(id) {
|
|
59
|
+
if (!this.canClose(id)) return false;
|
|
60
|
+
const index = this.ids.indexOf(id);
|
|
61
|
+
this.ids.splice(index, 1);
|
|
62
|
+
if (this.model.currentViewId === id) this.model.switchTo(this.ids[Math.min(index, this.ids.length - 1)]);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run `fn` with the list paused: going through views (a report's pictures) opens none. */
|
|
67
|
+
while(fn) {
|
|
68
|
+
this._paused++;
|
|
69
|
+
try { return fn(); } finally { this._paused--; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toJSON() {
|
|
73
|
+
return [...this.ids];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Every finding. @returns {Array<Object>} */
|
|
24
|
-
run() {
|
|
25
|
-
return this.checks().flatMap(check => check.call(this));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Views with nothing on them (a template is left out: an empty one is still a start). */
|
|
29
|
-
emptyViews() {
|
|
30
|
-
return this.model.views()
|
|
31
|
-
.filter(v => !v.template && v.isEmpty())
|
|
32
|
-
.map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
import { ModelChecker } from '../ModelChecker.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ViewSetChecker — checks over any ViewSetModel (a template editor's
|
|
5
|
+
* DiagramModel, an ArchimateModel): what is wrong or worth a look across its
|
|
6
|
+
* views (a ModelChecker: findings, checks listed by `checks()`). A notation
|
|
7
|
+
* adds its own checks in a subclass (ArchimateModelChecker). `kind` in a
|
|
8
|
+
* finding is 'view' here; a subclass adds its own.
|
|
9
|
+
*/
|
|
10
|
+
export class ViewSetChecker extends ModelChecker {
|
|
11
|
+
/** The checks to run, in the order their findings are listed. */
|
|
12
|
+
checks() {
|
|
13
|
+
return [this.emptyViews];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Views with nothing on them (a template is left out: an empty one is still a start). */
|
|
17
|
+
emptyViews() {
|
|
18
|
+
return this.model.views()
|
|
19
|
+
.filter(v => !v.template && v.isEmpty())
|
|
20
|
+
.map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
|
|
21
|
+
}
|
|
22
|
+
}
|