@peter.prib/renderengine 0.2.7 → 0.2.11
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 +66 -9
- package/REFERENCE.md +129 -0
- package/dist/renderengine-workspace.umd.js +5068 -2892
- package/dist/renderengine.umd.js +4874 -2888
- package/examples/data-model.html +40 -18
- package/package.json +1 -1
- package/src/charting/FormDialog.js +398 -385
- package/src/charting/RowsField.js +127 -0
- package/src/charting/datamodel/ClassificationLens.js +33 -0
- package/src/charting/datamodel/ClassificationScheme.js +98 -0
- package/src/charting/datamodel/DataModel.js +220 -4
- package/src/charting/datamodel/DataModelChecker.js +244 -2
- package/src/charting/datamodel/DataModelContext.js +40 -10
- package/src/charting/datamodel/DataModelEditCommand.js +47 -0
- package/src/charting/datamodel/DataModelPerspective.js +12 -2
- package/src/charting/datamodel/EnumerationType.js +64 -51
- package/src/charting/datamodel/LevelSchema.js +23 -0
- package/src/charting/datamodel/ServiceDomain.js +9 -0
- package/src/charting/datamodel/SqlDdl.js +85 -0
- package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
- package/src/charting/datamodel/index.js +21 -15
- package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
- package/src/charting/datamodel/naming/NameStyle.js +57 -0
- package/src/charting/datamodel/naming/NamingStandard.js +132 -0
- package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
- package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
- package/src/charting/datamodel/naming/index.js +8 -0
- package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
- package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
- package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
- package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
- package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
- package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
- package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
- package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
- package/src/charting/datamodel/scorecard/index.js +8 -0
- package/src/charting/diagrams/DataModelDiagram.js +4 -11
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +7 -1
- package/src/charting/diagrams/NodeGeometry.js +6 -2
- package/src/charting/toolbar/ToolbarSelect.js +6 -0
- package/src/datamodel.js +402 -4
- package/src/index.js +2666 -2666
- package/src/workspace/parts/DataModelPart.js +197 -6
|
@@ -7,12 +7,13 @@ import { ModelCheckDialog } from '../../charting/ModelCheckDialog.js';
|
|
|
7
7
|
import { DataModelNotation } from '../../datamodel.js';
|
|
8
8
|
import {
|
|
9
9
|
DataModel, DataModelPerspective, DataModelContext, DataModelFileFormat, ServiceDomain, ServiceLandscape, BianLandscapeReader,
|
|
10
|
-
BianSemanticApiReader, DataType, EnumerationType, StructuredType,
|
|
10
|
+
BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, ClassificationScheme, ClassificationLens,
|
|
11
|
+
SqlDdlFileFormat, ModelScorecard,
|
|
11
12
|
} from '../../charting/datamodel/index.js';
|
|
12
13
|
import { pickFile } from '../../charting/toolbar/pickFile.js';
|
|
13
14
|
import { ModelFile } from './ModelFile.js';
|
|
14
15
|
import { EditorPart } from './EditorPart.js';
|
|
15
|
-
import { downloadPng, downloadSvg } from '../../exportImage.js';
|
|
16
|
+
import { downloadPng, downloadSvg, downloadBlob } from '../../exportImage.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* DataModelPart — a data model (DataModel, drawn by dataModelDiagram) in a
|
|
@@ -96,7 +97,13 @@ export class DataModelPart extends ChartPart {
|
|
|
96
97
|
this._adoptModel(model);
|
|
97
98
|
this.perspectiveId = null;
|
|
98
99
|
this.contextId = null;
|
|
99
|
-
if (this.mounted)
|
|
100
|
+
if (!this.mounted) return;
|
|
101
|
+
// Not rendered again (that would add a second toolbar and canvas): the toolbar
|
|
102
|
+
// shows the new model's choices, and it is drawn. What was on screen is the
|
|
103
|
+
// other model's, so where its boxes were is not kept into this one.
|
|
104
|
+
this._drawnLevel = null;
|
|
105
|
+
this._changed();
|
|
106
|
+
this._redraw();
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/** A model opened from a file in a host's new tab (as EditorPart does). */
|
|
@@ -208,9 +215,131 @@ export class DataModelPart extends ChartPart {
|
|
|
208
215
|
...this.chartOptions,
|
|
209
216
|
...this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: this.level }),
|
|
210
217
|
...(this.level === DataModel.LANDSCAPE && { onNodeDoubleClick: (node) => this.openServiceDomain(ServiceLandscape.serviceDomainOf(node.id)) }),
|
|
218
|
+
...(this.editsLevel && this.editingOptions()),
|
|
219
|
+
...(this.level === DataModel.MAPPING && this.lineageOptions()),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Show an entity's lineage: the mapping view, what it realises and what
|
|
225
|
+
* realises it (DataModel#lineage) as they are, the rest greyed.
|
|
226
|
+
*/
|
|
227
|
+
showLineage(level, id) {
|
|
228
|
+
this.lineageFocus = { level, id };
|
|
229
|
+
this.setLevel(DataModel.MAPPING);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* In the mapping view: an entity's lineage picked out (`lineageFocus`), and
|
|
234
|
+
* its menus to show another's or show all again.
|
|
235
|
+
*/
|
|
236
|
+
lineageOptions() {
|
|
237
|
+
const view = {};
|
|
238
|
+
const focus = this.lineageFocus;
|
|
239
|
+
if (focus) {
|
|
240
|
+
const { up, down } = this.model.lineage(focus.level, focus.id);
|
|
241
|
+
const on = new Set([`${focus.level}:${focus.id}`, ...[...up, ...down].map(({ level, entity }) => `${level}:${entity.id}`)]);
|
|
242
|
+
const drawn = this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: DataModel.MAPPING });
|
|
243
|
+
view.entities = drawn.entities.map(e => (on.has(e.id) ? e : { ...e, color: '#f3f4f6' }));
|
|
244
|
+
view.links = drawn.links.map(l => (on.has(l.source) && on.has(l.target) ? l : { ...l, color: '#e5e7eb' }));
|
|
245
|
+
}
|
|
246
|
+
const split = (nodeId) => { const i = String(nodeId).indexOf(':'); return [nodeId.slice(0, i), nodeId.slice(i + 1)]; };
|
|
247
|
+
return {
|
|
248
|
+
...view,
|
|
249
|
+
contextMenuItems: [{ label: 'Show lineage', action: (node) => { const [level, id] = split(node.id); this.showLineage(level, id); } }],
|
|
250
|
+
canvasContextMenuItems: [{ label: 'Show all', action: () => { this.lineageFocus = null; this._redraw(); } }],
|
|
211
251
|
};
|
|
212
252
|
}
|
|
213
253
|
|
|
254
|
+
/** Whether what is shown is a level of the schema (so its entities and relationships can be edited here). */
|
|
255
|
+
get editsLevel() {
|
|
256
|
+
return !DataModel.otherView(this.level);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* At a level of the schema: double-click an entity or a relationship to
|
|
261
|
+
* edit it; right-click one to edit or delete it, or the canvas for a new
|
|
262
|
+
* entity there or a new relationship. (These stand in for graphDiagram's
|
|
263
|
+
* own, which change only the drawing.)
|
|
264
|
+
*/
|
|
265
|
+
editingOptions() {
|
|
266
|
+
const relationshipOf = (drawn) => this.model.relationshipIndex(this.level, drawn);
|
|
267
|
+
const isEntity = (node) => !!this.model.level(this.level).entity(node.id);
|
|
268
|
+
return {
|
|
269
|
+
onNodeDoubleClick: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))),
|
|
270
|
+
onEdgeDoubleClick: (edge) => this.editRelationship(relationshipOf(edge)),
|
|
271
|
+
contextMenuItems: [
|
|
272
|
+
{ label: 'Edit…', action: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))) },
|
|
273
|
+
{ label: 'Delete', action: (node) => (isEntity(node) ? this.removeEntity(node.id) : this.removeRelationship(relationshipOf(node))) },
|
|
274
|
+
{ label: 'Show lineage', action: (node) => { if (isEntity(node)) this.showLineage(this.level, node.id); } },
|
|
275
|
+
],
|
|
276
|
+
edgeContextMenuItems: [
|
|
277
|
+
{ label: 'Edit relationship…', action: (edge) => this.editRelationship(relationshipOf(edge)) },
|
|
278
|
+
{ label: 'Delete relationship', action: (edge) => this.removeRelationship(relationshipOf(edge)) },
|
|
279
|
+
],
|
|
280
|
+
canvasContextMenuItems: [
|
|
281
|
+
{ label: `New ${this.levelInfo.entityNoun.toLowerCase()} here…`, action: (subject, engine, point) => this.editEntity(null, point) },
|
|
282
|
+
{ label: 'New relationship…', action: () => this.editRelationship(null) },
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The level shown, as DataModelNotation has it (its nouns, its dialogs' fields). */
|
|
288
|
+
get levelInfo() {
|
|
289
|
+
return DataModelNotation.level(this.level);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* An entity's dialog at the level shown (none: a new one, placed at
|
|
294
|
+
* `point` or below the others): its level's fields (name, definition, and
|
|
295
|
+
* at the logical and physical levels its attributes as a table). Saved, it
|
|
296
|
+
* is one undo step.
|
|
297
|
+
*/
|
|
298
|
+
editEntity(id, point = null) {
|
|
299
|
+
if (!this.editsLevel) return null;
|
|
300
|
+
const info = this.levelInfo;
|
|
301
|
+
const schema = this.model.level(this.level);
|
|
302
|
+
const entity = id ? schema.entity(id) : null;
|
|
303
|
+
if (id && !entity) return null;
|
|
304
|
+
const scheme = this.model.classificationScheme;
|
|
305
|
+
const context = { typeNames: this.model.dataTypes.map(t => t.name), sensitivities: scheme.levels.map(l => l.name), categories: scheme.categories };
|
|
306
|
+
const title = entity ? `${info.entityNoun}: ${entity.name}` : `New ${info.entityNoun.toLowerCase()}`;
|
|
307
|
+
return this._dialog(title, info.entityFields(entity, context), (values) => {
|
|
308
|
+
const spec = info.entitySpecFrom(values, entity);
|
|
309
|
+
if (!entity) Object.assign(spec, point ?? this._freeSpot(schema));
|
|
310
|
+
this.runModelEdit(this.model.saveEntityCommand(this.level, id, spec));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Where a new entity goes with no point given: below what is there. @private */
|
|
315
|
+
_freeSpot(schema) {
|
|
316
|
+
const ys = schema.entities.map(e => e.y).filter(Number.isFinite);
|
|
317
|
+
return { x: 160, y: (ys.length ? Math.max(...ys) : 0) + 160 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
removeEntity(id) {
|
|
321
|
+
if (!this.editsLevel || !this.model.level(this.level).entity(id)) return null;
|
|
322
|
+
return this.runModelEdit(this.model.removeEntityCommand(this.level, id));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A relationship's dialog at the level shown (`index` null or -1: a new one); saved, one undo step. */
|
|
326
|
+
editRelationship(index) {
|
|
327
|
+
if (!this.editsLevel) return null;
|
|
328
|
+
const info = this.levelInfo;
|
|
329
|
+
const schema = this.model.level(this.level);
|
|
330
|
+
const relationship = index == null || index < 0 ? null : schema.relationships[index];
|
|
331
|
+
if (!schema.entities.length) return null;
|
|
332
|
+
const title = relationship ? `Relationship: ${relationship.name || `${relationship.source} – ${relationship.target}`}` : 'New relationship';
|
|
333
|
+
return this._dialog(title, info.relationshipFields(schema.entities, relationship), (values) => {
|
|
334
|
+
this.runModelEdit(this.model.saveRelationshipCommand(this.level, relationship ? index : null, info.relationshipSpecFrom(values, relationship)));
|
|
335
|
+
}, ({ source, target }) => (source && target ? null : 'Choose both ends'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
removeRelationship(index) {
|
|
339
|
+
if (!this.editsLevel || index == null || index < 0) return null;
|
|
340
|
+
return this.runModelEdit(this.model.removeRelationshipCommand(this.level, index));
|
|
341
|
+
}
|
|
342
|
+
|
|
214
343
|
/** See the model through a service domain (an id), at the logical level, where its business objects are. */
|
|
215
344
|
openServiceDomain(id) {
|
|
216
345
|
if (!this.model.perspective(id)) return;
|
|
@@ -260,8 +389,12 @@ export class DataModelPart extends ChartPart {
|
|
|
260
389
|
/** Model ▸: make, change or remove a perspective or a context (the one chosen). */
|
|
261
390
|
modelItems() {
|
|
262
391
|
return [
|
|
392
|
+
{ label: `New ${this.editsLevel ? this.levelInfo.entityNoun.toLowerCase() : 'entity'}…`, disabled: !this.editsLevel || undefined, action: () => this.editEntity(null) },
|
|
393
|
+
{ label: 'New relationship…', disabled: !this.editsLevel || !this.model.level(this.level).entities.length || undefined, action: () => this.editRelationship(null) },
|
|
394
|
+
{ separator: true },
|
|
263
395
|
{ label: 'New perspective…', action: () => this.editPerspective(null) },
|
|
264
396
|
{ label: 'New service domain…', action: () => this.editPerspective(null, ServiceDomain) },
|
|
397
|
+
{ label: 'New classification lens…', action: () => this.editPerspective(null, ClassificationLens) },
|
|
265
398
|
{ label: 'Read service domains from a BIAN landscape…', action: () => this.readLandscape() },
|
|
266
399
|
{ label: 'Read a BIAN service domain (semantic API)…', action: () => this.readSemanticApi() },
|
|
267
400
|
{ label: 'Edit perspective…', disabled: !this.perspective || undefined, action: () => this.editPerspective(this.perspective) },
|
|
@@ -274,6 +407,9 @@ export class DataModelPart extends ChartPart {
|
|
|
274
407
|
{ label: 'Delete context', disabled: !this.context || undefined, action: () => this.removeContext(this.contextId) },
|
|
275
408
|
{ separator: true },
|
|
276
409
|
{ label: 'Check model…', action: () => this.checkModel() },
|
|
410
|
+
{ label: 'Scorecard…', action: () => this.scoreModel() },
|
|
411
|
+
...EditorPart.menuGroup('Naming standard', this.namingItems()),
|
|
412
|
+
{ label: 'Classification scheme…', action: () => this.editClassificationScheme() },
|
|
277
413
|
];
|
|
278
414
|
}
|
|
279
415
|
|
|
@@ -365,6 +501,51 @@ export class DataModelPart extends ChartPart {
|
|
|
365
501
|
return command;
|
|
366
502
|
}
|
|
367
503
|
|
|
504
|
+
/**
|
|
505
|
+
* The Data Model Scorecard (ModelScorecard): each category as the model
|
|
506
|
+
* measures it, a reviewer's score and note, its weight; the total in the
|
|
507
|
+
* title. Saved, the reviews and weights are kept (one undo step).
|
|
508
|
+
*/
|
|
509
|
+
scoreModel() {
|
|
510
|
+
const card = this.model.scored();
|
|
511
|
+
const results = card.results();
|
|
512
|
+
const { score, coverage } = card.total(results);
|
|
513
|
+
const title = score == null ? 'Scorecard: nothing scored yet' : `Scorecard: ${score} of 100 (${coverage}% of the weight scored)`;
|
|
514
|
+
return this._dialog(title, card.fields(results), (values) => {
|
|
515
|
+
this.runModelEdit(this.model.scorecardCommand(ModelScorecard.specFrom(values)));
|
|
516
|
+
}, () => null);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** The classification scheme's dialog (its sensitivity levels in order, its categories); saved, one undo step. */
|
|
520
|
+
editClassificationScheme() {
|
|
521
|
+
return this._dialog('Classification scheme', ClassificationScheme.fields(this.model.classificationScheme), (values) => {
|
|
522
|
+
this.runModelEdit(this.model.classificationSchemeCommand(ClassificationScheme.specFrom(values)));
|
|
523
|
+
}, (values) => (ClassificationScheme.specFrom(values).levels.length ? null : 'Give it at least one level'));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Model ▸ Naming standard ▸: edit it, start from the conventional one, or remove it (Check model… checks names against it). */
|
|
527
|
+
namingItems() {
|
|
528
|
+
const has = !!this.model.namingStandard;
|
|
529
|
+
return [
|
|
530
|
+
{ label: has ? 'Edit…' : 'Set…', action: () => this.editNamingStandard() },
|
|
531
|
+
...(has ? [] : [{ label: 'Use the conventional one', action: () => this.runModelEdit(this.model.namingStandardCommand(NamingStandard.CONVENTIONAL)) }]),
|
|
532
|
+
...(has ? [{ label: 'Remove', action: () => this.runModelEdit(this.model.namingStandardCommand(null)) }] : []),
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The naming standard's dialog (NamingStandard.fields: a rule per level for
|
|
538
|
+
* entities and attributes); saved, one undo step. A standard that asks
|
|
539
|
+
* nothing is none.
|
|
540
|
+
*/
|
|
541
|
+
editNamingStandard() {
|
|
542
|
+
return this._dialog('Naming standard', NamingStandard.fields(this.model.namingStandard), (values) => {
|
|
543
|
+
const spec = NamingStandard.specFrom(values);
|
|
544
|
+
const asks = Object.values(spec).some(kinds => Object.values(kinds).some(r => r.style || r.classWords.length || r.maxLength || Object.keys(r.abbreviations).length));
|
|
545
|
+
this.runModelEdit(this.model.namingStandardCommand(asks ? spec : null));
|
|
546
|
+
}, () => null);
|
|
547
|
+
}
|
|
548
|
+
|
|
368
549
|
/** Model ▸ Data types ▸: a new one of each kind, then each to change or remove. */
|
|
369
550
|
dataTypeItems() {
|
|
370
551
|
const types = this.model.dataTypes;
|
|
@@ -454,12 +635,11 @@ export class DataModelPart extends ChartPart {
|
|
|
454
635
|
}
|
|
455
636
|
|
|
456
637
|
/** Open a form (a name is needed). @private */
|
|
457
|
-
_dialog(title, fields, apply) {
|
|
638
|
+
_dialog(title, fields, apply, validate = ({ name }) => ((name ?? '').trim() ? null : 'Give it a name')) {
|
|
458
639
|
return new FormDialog({
|
|
459
640
|
title, fields, submitLabel: 'Save', idPrefix: 'data-model-',
|
|
460
641
|
document: this.container?.ownerDocument ?? globalThis.document,
|
|
461
|
-
validate
|
|
462
|
-
onApply: apply,
|
|
642
|
+
validate, onApply: apply,
|
|
463
643
|
}).open();
|
|
464
644
|
}
|
|
465
645
|
|
|
@@ -475,12 +655,23 @@ export class DataModelPart extends ChartPart {
|
|
|
475
655
|
return [
|
|
476
656
|
{ label: 'Image (PNG)', action: () => downloadPng(this.canvasHost, { name }) },
|
|
477
657
|
{ label: 'Image (SVG)', action: () => downloadSvg(this.canvasHost, { name }) },
|
|
658
|
+
{ separator: true },
|
|
659
|
+
{ label: 'SQL DDL (physical level)', action: () => this.exportDdl() },
|
|
478
660
|
];
|
|
479
661
|
}
|
|
480
662
|
|
|
663
|
+
/** The physical level as SQL DDL (SqlDdlFileFormat), downloaded as the model's name. Returns the file. */
|
|
664
|
+
exportDdl({ download = (file, fileName) => downloadBlob(new (this.document.defaultView ?? globalThis).Blob([file.data], { type: file.mimeType }), fileName) } = {}) {
|
|
665
|
+
const file = new SqlDdlFileFormat().export(this.model);
|
|
666
|
+
download(file, `${String(this.model.name || this.title || 'model').replace(/[^A-Za-z0-9._-]+/g, '-')}.sql`);
|
|
667
|
+
return file;
|
|
668
|
+
}
|
|
669
|
+
|
|
481
670
|
/** Redraw now (a choice made), when there is somewhere to draw. @private */
|
|
482
671
|
_redraw() {
|
|
483
672
|
if (this.canvasHost) this._draw();
|
|
673
|
+
// Its toolbar shows what is drawn, however it was chosen (a finding, a lineage, a service domain opened).
|
|
674
|
+
this.menuBar?.refresh?.();
|
|
484
675
|
}
|
|
485
676
|
|
|
486
677
|
/**
|