@peter.prib/renderengine 0.2.10 → 0.4.0
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 +22 -6
- package/REFERENCE.md +123 -2
- package/dist/renderengine-workspace.umd.js +3015 -1221
- package/dist/renderengine.umd.js +1220 -176
- package/package.json +1 -1
- package/src/charting/ModelCheckDialog.js +10 -3
- package/src/charting/ModelComparisonDialog.js +10 -3
- package/src/charting/ModelFileFormat.js +4 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +14 -2
- package/src/charting/archimate/ArchimateElementTemplate.js +23 -0
- package/src/charting/arrangement/layouts.js +9 -4
- package/src/charting/datamodel/DataModel.js +18 -71
- package/src/charting/datamodel/DataModelChecker.js +1 -5
- package/src/charting/datamodel/DataModelCompared.js +1 -31
- package/src/charting/datamodel/LevelSchema.js +23 -0
- package/src/charting/datamodel/SqlDdl.js +18 -13
- package/src/charting/diagramModel/ListCompared.js +35 -0
- package/src/charting/model/Benchmark.js +59 -0
- package/src/charting/model/EditableModel.js +104 -0
- package/src/charting/{datamodel/DataModelEditCommand.js → model/ModelEditCommand.js} +47 -47
- package/src/charting/requirements/RequirementsChecker.js +76 -0
- package/src/charting/requirements/RequirementsCompared.js +33 -0
- package/src/charting/requirements/RequirementsFileFormat.js +32 -0
- package/src/charting/requirements/RequirementsImportFormats.js +48 -0
- package/src/charting/requirements/RequirementsModel.js +305 -0
- package/src/charting/requirements/Storyboard.js +94 -0
- package/src/charting/requirements/TrackerExport.js +273 -0
- package/src/charting/requirements/index.js +7 -0
- package/src/index.js +6 -0
- package/src/workspace/Workspace.js +329 -321
- package/src/workspace/index.js +2 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +31 -1
- package/src/workspace/parts/DataModelPart.js +668 -735
- package/src/workspace/parts/EditorPart.js +1 -1
- package/src/workspace/parts/ModelFile.js +3 -1
- package/src/workspace/parts/ModelHosted.js +176 -0
- package/src/workspace/parts/ModelsPart.js +146 -7
- package/src/workspace/parts/RequirementsPart.js +465 -0
- package/src/workspace/storage/ModelLibrary.js +8 -2
|
@@ -1,735 +1,668 @@
|
|
|
1
|
-
import { ChartPart } from './ChartPart.js';
|
|
2
|
-
import { el } from '../dom.js';
|
|
3
|
-
import { MenuBar } from '../../charting/MenuBar.js';
|
|
4
|
-
import { ToolbarSelect, ToolbarMenu, ToolbarText } from '../../charting/toolbar/index.js';
|
|
5
|
-
import { FormDialog } from '../../charting/FormDialog.js';
|
|
6
|
-
import { ModelCheckDialog } from '../../charting/ModelCheckDialog.js';
|
|
7
|
-
import { DataModelNotation } from '../../datamodel.js';
|
|
8
|
-
import {
|
|
9
|
-
DataModel, DataModelPerspective, DataModelContext, DataModelFileFormat, ServiceDomain, ServiceLandscape, BianLandscapeReader,
|
|
10
|
-
BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, ClassificationScheme, ClassificationLens,
|
|
11
|
-
SqlDdlFileFormat, ModelScorecard,
|
|
12
|
-
} from '../../charting/datamodel/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
/**
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
/** The
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
this.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
this.
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
this.
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
/** Redraw now (a choice made), when there is somewhere to draw. @private */
|
|
671
|
-
_redraw() {
|
|
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?.();
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Keep where the boxes and notes were dragged (the drawing is made anew each
|
|
679
|
-
* time): on the level shown, or in the mapping view's own layout. Then draw.
|
|
680
|
-
*/
|
|
681
|
-
_draw() {
|
|
682
|
-
if (this._keepPlaces()) this.model.edited(); // a box or note moved is a change, kept and saved
|
|
683
|
-
this._drawnLevel = this.level;
|
|
684
|
-
super._draw();
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Keep where the drawing on screen has its boxes and notes: on the level
|
|
689
|
-
* it shows, or in the mapping view's layout. Returns whether any moved. @private
|
|
690
|
-
*/
|
|
691
|
-
_keepPlaces() {
|
|
692
|
-
const pos = this.engine?._graph?.pos;
|
|
693
|
-
if (!pos || !this._drawnLevel) return false;
|
|
694
|
-
let moved = false;
|
|
695
|
-
const place = (p, before, put) => {
|
|
696
|
-
if (!p || (before && before.x === p.x && before.y === p.y)) return;
|
|
697
|
-
put({ x: p.x, y: p.y });
|
|
698
|
-
moved = true;
|
|
699
|
-
};
|
|
700
|
-
const other = DataModel.otherView(this._drawnLevel);
|
|
701
|
-
if (other && !other.keepsPlaces) return false; // laid out by its structure, anew each time
|
|
702
|
-
if (other) {
|
|
703
|
-
for (const [id, p] of pos) {
|
|
704
|
-
if (id.includes(':')) place(p, this.model.mappingLayout[id], (at) => { this.model.mappingLayout[id] = at; });
|
|
705
|
-
}
|
|
706
|
-
return moved;
|
|
707
|
-
}
|
|
708
|
-
for (const entity of this.model.level(this._drawnLevel).entities) {
|
|
709
|
-
place(pos.get(entity.id), entity, (at) => Object.assign(entity, at));
|
|
710
|
-
}
|
|
711
|
-
for (const rule of this.model.contexts.flatMap(c => c.constraints)) {
|
|
712
|
-
const p = pos.get(rule.id);
|
|
713
|
-
const before = this.model.notePlace(this._drawnLevel, rule);
|
|
714
|
-
place(p, Number.isFinite(before.x) ? before : null, (at) => this.model.placeNote(this._drawnLevel, rule, at));
|
|
715
|
-
}
|
|
716
|
-
return moved;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
dispose() {
|
|
720
|
-
this._unsubscribeEdits?.();
|
|
721
|
-
this.file.restoreTitle();
|
|
722
|
-
this.menuBar?.dispose?.();
|
|
723
|
-
super.dispose();
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/** The spec again: the model (with where its boxes are), what is chosen, and the other options. */
|
|
727
|
-
serialize() {
|
|
728
|
-
const { options, chartType, ...base } = super.serialize();
|
|
729
|
-
const { level, notation, ...rest } = options;
|
|
730
|
-
return {
|
|
731
|
-
...base, ...this.model.toJSON(),
|
|
732
|
-
perspective: this.perspectiveId, context: this.contextId, level, notation, options: rest,
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
}
|
|
1
|
+
import { ChartPart } from './ChartPart.js';
|
|
2
|
+
import { el } from '../dom.js';
|
|
3
|
+
import { MenuBar } from '../../charting/MenuBar.js';
|
|
4
|
+
import { ToolbarSelect, ToolbarMenu, ToolbarText } from '../../charting/toolbar/index.js';
|
|
5
|
+
import { FormDialog } from '../../charting/FormDialog.js';
|
|
6
|
+
import { ModelCheckDialog } from '../../charting/ModelCheckDialog.js';
|
|
7
|
+
import { DataModelNotation } from '../../datamodel.js';
|
|
8
|
+
import {
|
|
9
|
+
DataModel, DataModelPerspective, DataModelContext, DataModelFileFormat, ServiceDomain, ServiceLandscape, BianLandscapeReader,
|
|
10
|
+
BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, ClassificationScheme, ClassificationLens,
|
|
11
|
+
SqlDdlFileFormat, ModelScorecard,
|
|
12
|
+
} from '../../charting/datamodel/index.js';
|
|
13
|
+
import { ModelHosted } from './ModelHosted.js';
|
|
14
|
+
import { EditorPart } from './EditorPart.js';
|
|
15
|
+
import { downloadPng, downloadSvg, downloadBlob } from '../../exportImage.js';
|
|
16
|
+
import { layout, LAYOUTS } from '../../charting/arrangement/layouts.js';
|
|
17
|
+
import { DataModelDiagram } from '../../charting/diagrams/DataModelDiagram.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DataModelPart — a data model (DataModel, drawn by dataModelDiagram) in a
|
|
21
|
+
* pane: its conceptual, logical and physical levels, each its own schema, and
|
|
22
|
+
* how they map; seen through a perspective (whose concepts, under whose names)
|
|
23
|
+
* read in a context (what they mean here, the rules that hold, where and
|
|
24
|
+
* when), in a notation (Chen, Crow's Foot, UML). Its toolbar switches each
|
|
25
|
+
* of them, Level offering the views that are no level (DataModel.OTHER_VIEWS:
|
|
26
|
+
* Mapping, the levels side by side joined where they map; Service landscape,
|
|
27
|
+
* the service domains in their business areas and domains, a double-click
|
|
28
|
+
* seeing the model through one), and its Model menu makes, changes and removes
|
|
29
|
+
* perspectives and contexts and reads service domains from a BIAN landscape. Boxes and notes stay where they were dragged. Export ▸ saves the
|
|
30
|
+
* picture (PNG, SVG). The drawing, its sizing and redrawing on resize are
|
|
31
|
+
* ChartPart's.
|
|
32
|
+
*
|
|
33
|
+
* It is saved as a file (DataModelFileFormat, `.datamodel.json`) as an
|
|
34
|
+
* editor's model is (ModelHosted: ModelFile): File ▸ Open…, Save, Save As… (Ctrl+O, Ctrl+S),
|
|
35
|
+
* • while changed since, and asking before the page is left with changes
|
|
36
|
+
* unsaved. Hosted by a ModelsPart (`editorType: 'data-model'`) it is one of
|
|
37
|
+
* several models open, a tab each, and with that part's `keep` it is kept in
|
|
38
|
+
* this browser and backed up (File ▸ This browser), as the modeller's are.
|
|
39
|
+
*
|
|
40
|
+
* Spec: `{ id, type: 'data-model', title?, levels?, mappings?, mappingLayout?,
|
|
41
|
+
* perspectives?, contexts?, perspective?, context?, level?, notation?, options? }`
|
|
42
|
+
* — the model as DataModel takes it (or `entities`/`relationships` in the older
|
|
43
|
+
* one-entity form); `perspective`/`context` the ids chosen; `level` default
|
|
44
|
+
* 'logical' ('mapping' for the mapping view), `notation` default 'crowsfoot';
|
|
45
|
+
* `options` anything else dataModelDiagram takes (editable, resizable by default).
|
|
46
|
+
*/
|
|
47
|
+
export class DataModelPart extends ModelHosted(ChartPart) {
|
|
48
|
+
static type = 'data-model';
|
|
49
|
+
|
|
50
|
+
constructor(spec = {}) {
|
|
51
|
+
const { perspective = null, context = null, level = 'logical', notation = 'crowsfoot', options = {} } = spec;
|
|
52
|
+
super({ ...spec, chartType: 'dataModel', options: { editable: true, resizable: true, ...options, level, notation } });
|
|
53
|
+
this._adoptModel(new DataModel(spec));
|
|
54
|
+
/** The ids chosen, or null. */
|
|
55
|
+
this.perspectiveId = perspective;
|
|
56
|
+
this.contextId = context ?? this.model.perspective(perspective)?.context ?? null;
|
|
57
|
+
/** The level the drawing on screen is of (where a drag is kept). */
|
|
58
|
+
this._drawnLevel = level;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===== A model saved as a file, kept, backed up =====
|
|
62
|
+
|
|
63
|
+
/** Its file formats: the data model's own. */
|
|
64
|
+
fileFormats() {
|
|
65
|
+
return [DataModelFileFormat.instance ??= new DataModelFileFormat()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Edit another model (one opened, imported or kept), from scratch: nothing chosen, nothing changed. */
|
|
69
|
+
replaceModel(model) {
|
|
70
|
+
this._adoptModel(model);
|
|
71
|
+
this.perspectiveId = null;
|
|
72
|
+
this.contextId = null;
|
|
73
|
+
if (!this.mounted) return;
|
|
74
|
+
// Not rendered again (that would add a second toolbar and canvas): the toolbar
|
|
75
|
+
// shows the new model's choices, and it is drawn. What was on screen is the
|
|
76
|
+
// other model's, so where its boxes were is not kept into this one.
|
|
77
|
+
this._drawnLevel = null;
|
|
78
|
+
this._drawnPlaces = null;
|
|
79
|
+
this._changed();
|
|
80
|
+
this._redraw();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Edit ▸: undo and redo (a merge is one step), and laying the level shown out again. */
|
|
84
|
+
editItems() {
|
|
85
|
+
return [
|
|
86
|
+
...this.undoItems(),
|
|
87
|
+
...(this.editsLevel ? [
|
|
88
|
+
{ separator: true },
|
|
89
|
+
{ label: 'Lay out', items: LAYOUTS.map(([kind, label]) => ({ label, action: () => this.runModelEdit(this.layoutCommand(kind)) })) },
|
|
90
|
+
] : []),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lay the boxes of the level shown out again — what is drawn there (the
|
|
96
|
+
* perspective's), by one of the arrangement's layouts (LAYOUTS: grid,
|
|
97
|
+
* circle, trees, force-directed, by kind), its lines guiding the trees and
|
|
98
|
+
* the force. Where each box goes is kept in the model as a drag would be.
|
|
99
|
+
* One undo step; null on a view that is no level of the schema.
|
|
100
|
+
* @param {string} kind - A LAYOUTS key
|
|
101
|
+
*/
|
|
102
|
+
layoutCommand(kind) {
|
|
103
|
+
if (!this.editsLevel) return null;
|
|
104
|
+
const level = this.level;
|
|
105
|
+
const view = this.model.view({ perspective: this.perspectiveId, context: this.contextId, level });
|
|
106
|
+
const notation = DataModelNotation.notation(this.notation);
|
|
107
|
+
const boxes = view.entities.map(e => {
|
|
108
|
+
const node = DataModelDiagram.buildNode({ ...e, x: Number.isFinite(e.x) ? e.x : 0, y: Number.isFinite(e.y) ? e.y : 0 }, { level, notation });
|
|
109
|
+
const b = DataModelDiagram.boxOf(node);
|
|
110
|
+
return { id: e.id, left: b.x0, top: b.y0, width: b.x1 - b.x0, height: b.y1 - b.y0, kind: e.role ?? e.kind ?? '' };
|
|
111
|
+
});
|
|
112
|
+
const placed = layout(kind, boxes, {
|
|
113
|
+
edges: (view.relationships ?? []).map(r => ({ source: r.source, target: r.target })),
|
|
114
|
+
categoryOf: (box) => box.kind,
|
|
115
|
+
});
|
|
116
|
+
const sizes = new Map(boxes.map(b => [b.id, b]));
|
|
117
|
+
const label = LAYOUTS.find(([k]) => k === kind)?.[1] ?? kind;
|
|
118
|
+
return this.model.editCommand(`Lay out: ${label}`, (model) => {
|
|
119
|
+
for (const p of placed) {
|
|
120
|
+
const entity = model.level(level).entity(p.id), b = sizes.get(p.id);
|
|
121
|
+
if (entity) Object.assign(entity, { x: Math.round(p.left + b.width / 2), y: Math.round(p.top + b.height / 2) });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An edit shown: the toolbar's choices anew, and drawn. */
|
|
127
|
+
afterModelEdit() {
|
|
128
|
+
this._changed();
|
|
129
|
+
this._redraw();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The entities at the level shown (the concepts in the mapping view). */
|
|
133
|
+
get entities() {
|
|
134
|
+
return this.model.level(DataModel.otherView(this.level) ? 'conceptual' : this.level).entities;
|
|
135
|
+
}
|
|
136
|
+
get level() { return this.chartOptions.level; }
|
|
137
|
+
get notation() { return this.chartOptions.notation; }
|
|
138
|
+
/** The perspective chosen (DataModelPerspective), or null. */
|
|
139
|
+
get perspective() { return this.model.perspective(this.perspectiveId); }
|
|
140
|
+
/** The context chosen (DataModelContext), or null. */
|
|
141
|
+
get context() { return this.model.context(this.contextId); }
|
|
142
|
+
|
|
143
|
+
/** Show another level ('conceptual', 'logical', 'physical'), or how they map ('mapping'). */
|
|
144
|
+
setLevel(level) {
|
|
145
|
+
this.chartOptions.level = level;
|
|
146
|
+
this._redraw();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Draw the model in another notation ('chen', 'crowsfoot', 'uml'). */
|
|
150
|
+
setNotation(notation) {
|
|
151
|
+
this.chartOptions.notation = notation;
|
|
152
|
+
this._redraw();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** See the model through a perspective (an id; null: the whole schema), in the context it is read in, if it names one. */
|
|
156
|
+
setPerspective(id) {
|
|
157
|
+
this.perspectiveId = id || null;
|
|
158
|
+
const own = this.perspective?.context;
|
|
159
|
+
if (own) this.contextId = own;
|
|
160
|
+
this._redraw();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Read the model in a context (an id; null: the schema's own descriptions). */
|
|
164
|
+
setContext(id) {
|
|
165
|
+
this.contextId = id || null;
|
|
166
|
+
this._redraw();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** What is drawn: the perspective's view of the level (or the mapping) in the context (DataModel#view), in the notation. */
|
|
170
|
+
drawOptions() {
|
|
171
|
+
return {
|
|
172
|
+
...this.chartOptions,
|
|
173
|
+
...this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: this.level }),
|
|
174
|
+
...(this.level === DataModel.LANDSCAPE && { onNodeDoubleClick: (node) => this.openServiceDomain(ServiceLandscape.serviceDomainOf(node.id)) }),
|
|
175
|
+
...(this.editsLevel && this.editingOptions()),
|
|
176
|
+
...(this.level === DataModel.MAPPING && this.lineageOptions()),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Show an entity's lineage: the mapping view, what it realises and what
|
|
182
|
+
* realises it (DataModel#lineage) as they are, the rest greyed.
|
|
183
|
+
*/
|
|
184
|
+
showLineage(level, id) {
|
|
185
|
+
this.lineageFocus = { level, id };
|
|
186
|
+
this.setLevel(DataModel.MAPPING);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* In the mapping view: an entity's lineage picked out (`lineageFocus`), and
|
|
191
|
+
* its menus to show another's or show all again.
|
|
192
|
+
*/
|
|
193
|
+
lineageOptions() {
|
|
194
|
+
const view = {};
|
|
195
|
+
const focus = this.lineageFocus;
|
|
196
|
+
if (focus) {
|
|
197
|
+
const { up, down } = this.model.lineage(focus.level, focus.id);
|
|
198
|
+
const on = new Set([`${focus.level}:${focus.id}`, ...[...up, ...down].map(({ level, entity }) => `${level}:${entity.id}`)]);
|
|
199
|
+
const drawn = this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: DataModel.MAPPING });
|
|
200
|
+
view.entities = drawn.entities.map(e => (on.has(e.id) ? e : { ...e, color: '#f3f4f6' }));
|
|
201
|
+
view.links = drawn.links.map(l => (on.has(l.source) && on.has(l.target) ? l : { ...l, color: '#e5e7eb' }));
|
|
202
|
+
}
|
|
203
|
+
const split = (nodeId) => { const i = String(nodeId).indexOf(':'); return [nodeId.slice(0, i), nodeId.slice(i + 1)]; };
|
|
204
|
+
return {
|
|
205
|
+
...view,
|
|
206
|
+
contextMenuItems: [{ label: 'Show lineage', action: (node) => { const [level, id] = split(node.id); this.showLineage(level, id); } }],
|
|
207
|
+
canvasContextMenuItems: [{ label: 'Show all', action: () => { this.lineageFocus = null; this._redraw(); } }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Whether what is shown is a level of the schema (so its entities and relationships can be edited here). */
|
|
212
|
+
get editsLevel() {
|
|
213
|
+
return !DataModel.otherView(this.level);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* At a level of the schema: double-click an entity or a relationship to
|
|
218
|
+
* edit it; right-click one to edit or delete it, or the canvas for a new
|
|
219
|
+
* entity there or a new relationship. (These stand in for graphDiagram's
|
|
220
|
+
* own, which change only the drawing.)
|
|
221
|
+
*/
|
|
222
|
+
editingOptions() {
|
|
223
|
+
const relationshipOf = (drawn) => this.model.relationshipIndex(this.level, drawn);
|
|
224
|
+
const isEntity = (node) => !!this.model.level(this.level).entity(node.id);
|
|
225
|
+
return {
|
|
226
|
+
onNodeDoubleClick: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))),
|
|
227
|
+
onEdgeDoubleClick: (edge) => this.editRelationship(relationshipOf(edge)),
|
|
228
|
+
contextMenuItems: [
|
|
229
|
+
{ label: 'Edit…', action: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))) },
|
|
230
|
+
{ label: 'Delete', action: (node) => (isEntity(node) ? this.removeEntity(node.id) : this.removeRelationship(relationshipOf(node))) },
|
|
231
|
+
{ label: 'Show lineage', action: (node) => { if (isEntity(node)) this.showLineage(this.level, node.id); } },
|
|
232
|
+
],
|
|
233
|
+
edgeContextMenuItems: [
|
|
234
|
+
{ label: 'Edit relationship…', action: (edge) => this.editRelationship(relationshipOf(edge)) },
|
|
235
|
+
{ label: 'Delete relationship', action: (edge) => this.removeRelationship(relationshipOf(edge)) },
|
|
236
|
+
],
|
|
237
|
+
canvasContextMenuItems: [
|
|
238
|
+
{ label: `New ${this.levelInfo.entityNoun.toLowerCase()} here…`, action: (subject, engine, point) => this.editEntity(null, point) },
|
|
239
|
+
{ label: 'New relationship…', action: () => this.editRelationship(null) },
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The level shown, as DataModelNotation has it (its nouns, its dialogs' fields). */
|
|
245
|
+
get levelInfo() {
|
|
246
|
+
return DataModelNotation.level(this.level);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* An entity's dialog at the level shown (none: a new one, placed at
|
|
251
|
+
* `point` or below the others): its level's fields (name, definition, and
|
|
252
|
+
* at the logical and physical levels its attributes as a table). Saved, it
|
|
253
|
+
* is one undo step.
|
|
254
|
+
*/
|
|
255
|
+
editEntity(id, point = null) {
|
|
256
|
+
if (!this.editsLevel) return null;
|
|
257
|
+
const info = this.levelInfo;
|
|
258
|
+
const schema = this.model.level(this.level);
|
|
259
|
+
const entity = id ? schema.entity(id) : null;
|
|
260
|
+
if (id && !entity) return null;
|
|
261
|
+
const scheme = this.model.classificationScheme;
|
|
262
|
+
const context = { typeNames: this.model.dataTypes.map(t => t.name), sensitivities: scheme.levels.map(l => l.name), categories: scheme.categories };
|
|
263
|
+
const title = entity ? `${info.entityNoun}: ${entity.name}` : `New ${info.entityNoun.toLowerCase()}`;
|
|
264
|
+
return this._dialog(title, info.entityFields(entity, context), (values) => {
|
|
265
|
+
const spec = info.entitySpecFrom(values, entity);
|
|
266
|
+
if (!entity) Object.assign(spec, point ?? this._freeSpot(schema));
|
|
267
|
+
this.runModelEdit(this.model.saveEntityCommand(this.level, id, spec));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Where a new entity goes with no point given: below what is there. @private */
|
|
272
|
+
_freeSpot(schema) {
|
|
273
|
+
const ys = schema.entities.map(e => e.y).filter(Number.isFinite);
|
|
274
|
+
return { x: 160, y: (ys.length ? Math.max(...ys) : 0) + 160 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
removeEntity(id) {
|
|
278
|
+
if (!this.editsLevel || !this.model.level(this.level).entity(id)) return null;
|
|
279
|
+
return this.runModelEdit(this.model.removeEntityCommand(this.level, id));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A relationship's dialog at the level shown (`index` null or -1: a new one); saved, one undo step. */
|
|
283
|
+
editRelationship(index) {
|
|
284
|
+
if (!this.editsLevel) return null;
|
|
285
|
+
const info = this.levelInfo;
|
|
286
|
+
const schema = this.model.level(this.level);
|
|
287
|
+
const relationship = index == null || index < 0 ? null : schema.relationships[index];
|
|
288
|
+
if (!schema.entities.length) return null;
|
|
289
|
+
const title = relationship ? `Relationship: ${relationship.name || `${relationship.source} – ${relationship.target}`}` : 'New relationship';
|
|
290
|
+
return this._dialog(title, info.relationshipFields(schema.entities, relationship), (values) => {
|
|
291
|
+
this.runModelEdit(this.model.saveRelationshipCommand(this.level, relationship ? index : null, info.relationshipSpecFrom(values, relationship)));
|
|
292
|
+
}, ({ source, target }) => (source && target ? null : 'Choose both ends'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
removeRelationship(index) {
|
|
296
|
+
if (!this.editsLevel || index == null || index < 0) return null;
|
|
297
|
+
return this.runModelEdit(this.model.removeRelationshipCommand(this.level, index));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** See the model through a service domain (an id), at the logical level, where its business objects are. */
|
|
301
|
+
openServiceDomain(id) {
|
|
302
|
+
if (!this.model.perspective(id)) return;
|
|
303
|
+
this.chartOptions.level = 'logical';
|
|
304
|
+
this.setPerspective(id);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
render() {
|
|
308
|
+
Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', height: '100%' });
|
|
309
|
+
const toolbar = this.container.appendChild(el({}));
|
|
310
|
+
super.render();
|
|
311
|
+
Object.assign(this.canvasHost.style, { flex: '1 1 auto', minHeight: '0', height: 'auto' });
|
|
312
|
+
this.menuBar = new MenuBar({ container: toolbar, items: this.menuBarItems(), document: this.container.ownerDocument }).render();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Model ▸ (perspectives and contexts of one's own), Perspective and Context
|
|
317
|
+
* (when the model has any; the context's scope beside them), Level (each
|
|
318
|
+
* level from DataModelNotation, then Mapping) and Notation, and Export ▸.
|
|
319
|
+
*/
|
|
320
|
+
menuBarItems() {
|
|
321
|
+
const { perspectives, contexts } = this.model;
|
|
322
|
+
const host = this.modelsHost;
|
|
323
|
+
return [
|
|
324
|
+
new ToolbarMenu('File', () => this.fileItems()),
|
|
325
|
+
new ToolbarMenu('Edit', () => this.editItems()),
|
|
326
|
+
...(host ? [new ToolbarMenu('Tools', () => host.toolItems(this))] : []),
|
|
327
|
+
new ToolbarMenu('Model', () => this.modelItems()),
|
|
328
|
+
...(perspectives.length ? [new ToolbarSelect('Perspective',
|
|
329
|
+
() => [['', 'Whole schema'], ...this.model.perspectives.map(p => [p.id, p.constructor === DataModelPerspective ? p.name : `${p.name} (${p.constructor.kindLabel.toLowerCase()})`, p.description])],
|
|
330
|
+
() => this.perspectiveId ?? '', (v) => this.setPerspective(v))] : []),
|
|
331
|
+
...(contexts.length ? [
|
|
332
|
+
new ToolbarSelect('Context',
|
|
333
|
+
() => [['', 'None'], ...this.model.contexts.map(c => [c.id, c.name, c.description])],
|
|
334
|
+
() => this.contextId ?? '', (v) => this.setContext(v)),
|
|
335
|
+
new ToolbarText(() => this.context?.scope ?? ''),
|
|
336
|
+
] : []),
|
|
337
|
+
new ToolbarSelect('Level', () => [
|
|
338
|
+
...DataModelNotation.levels().map(l => [l.name, l.label, l.description]),
|
|
339
|
+
...DataModel.OTHER_VIEWS.filter(v => v.offered(this.model) || v.name === this.level).map(v => [v.name, v.label, v.description]),
|
|
340
|
+
], () => this.level, (v) => this.setLevel(v)),
|
|
341
|
+
new ToolbarSelect('Notation', () => DataModelNotation.notations().map(n => [n.name, n.label]), () => this.notation, (v) => this.setNotation(v)),
|
|
342
|
+
new ToolbarMenu('Export', () => this.exportItems()),
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Model ▸: make, change or remove a perspective or a context (the one chosen). */
|
|
347
|
+
modelItems() {
|
|
348
|
+
return [
|
|
349
|
+
{ label: `New ${this.editsLevel ? this.levelInfo.entityNoun.toLowerCase() : 'entity'}…`, disabled: !this.editsLevel || undefined, action: () => this.editEntity(null) },
|
|
350
|
+
{ label: 'New relationship…', disabled: !this.editsLevel || !this.model.level(this.level).entities.length || undefined, action: () => this.editRelationship(null) },
|
|
351
|
+
{ separator: true },
|
|
352
|
+
{ label: 'New perspective…', action: () => this.editPerspective(null) },
|
|
353
|
+
{ label: 'New service domain…', action: () => this.editPerspective(null, ServiceDomain) },
|
|
354
|
+
{ label: 'New classification lens…', action: () => this.editPerspective(null, ClassificationLens) },
|
|
355
|
+
{ label: 'Read service domains from a BIAN landscape…', action: () => this.readLandscape() },
|
|
356
|
+
{ label: 'Read a BIAN service domain (semantic API)…', action: () => this.readSemanticApi() },
|
|
357
|
+
{ label: 'Edit perspective…', disabled: !this.perspective || undefined, action: () => this.editPerspective(this.perspective) },
|
|
358
|
+
{ label: 'Delete perspective', disabled: !this.perspective || undefined, action: () => this.removePerspective(this.perspectiveId) },
|
|
359
|
+
{ separator: true },
|
|
360
|
+
...EditorPart.menuGroup('Data types', this.dataTypeItems()),
|
|
361
|
+
{ separator: true },
|
|
362
|
+
{ label: 'New context…', action: () => this.editContext(null) },
|
|
363
|
+
{ label: 'Edit context…', disabled: !this.context || undefined, action: () => this.editContext(this.context) },
|
|
364
|
+
{ label: 'Delete context', disabled: !this.context || undefined, action: () => this.removeContext(this.contextId) },
|
|
365
|
+
{ separator: true },
|
|
366
|
+
{ label: 'Check model…', action: () => this.checkModel() },
|
|
367
|
+
{ label: 'Scorecard…', action: () => this.scoreModel() },
|
|
368
|
+
...EditorPart.menuGroup('Naming standard', this.namingItems()),
|
|
369
|
+
{ label: 'Classification scheme…', action: () => this.editClassificationScheme() },
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Service domains from BIAN's published service landscape (an ArchiMate
|
|
375
|
+
* model: an Archi file or the Exchange Format; BianLandscapeReader), as one
|
|
376
|
+
* undo step (DataModel#serviceDomainsCommand); then the landscape is shown.
|
|
377
|
+
* @param {Object} [options] - As readInto's, and `reader` (a BianLandscapeReader)
|
|
378
|
+
* @returns {Promise<Command|null>}
|
|
379
|
+
*/
|
|
380
|
+
readLandscape({ reader = new BianLandscapeReader(), ...options } = {}) {
|
|
381
|
+
let found = 0;
|
|
382
|
+
return this.readInto({
|
|
383
|
+
...options, accept: '.archimate,.xml', what: 'the landscape',
|
|
384
|
+
command: (bytes, name) => {
|
|
385
|
+
const specs = reader.read(BianLandscapeReader.parse(bytes));
|
|
386
|
+
found = specs.length;
|
|
387
|
+
return this.model.serviceDomainsCommand(specs, `"${name}"`);
|
|
388
|
+
},
|
|
389
|
+
nothing: (name) => (found
|
|
390
|
+
? `"${name}" brings nothing new: its ${found} service domain${found === 1 ? ' is' : 's are'} here as ${found === 1 ? 'it is' : 'they are'}.`
|
|
391
|
+
: `"${name}" has no service domains (Capabilities with none inside them).`),
|
|
392
|
+
then: () => { this.chartOptions.level = DataModel.LANDSCAPE; this.perspectiveId = null; },
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A service domain from one of BIAN's semantic APIs (an OpenAPI file, YAML
|
|
398
|
+
* or JSON; BianSemanticApiReader): its control record and behaviour
|
|
399
|
+
* qualifiers as logical entities, the schemas they use as data types, and
|
|
400
|
+
* the service domain, as one undo step (DataModel#bringCommand); then the
|
|
401
|
+
* model is seen through it at the logical level. An entity the model has
|
|
402
|
+
* by name stays as it is and is used.
|
|
403
|
+
* @param {Object} [options] - As readInto's, and `depth` (BianSemanticApiReader's)
|
|
404
|
+
* @returns {Promise<Command|null>}
|
|
405
|
+
*/
|
|
406
|
+
readSemanticApi({ depth = 1, ...options } = {}) {
|
|
407
|
+
let read = null;
|
|
408
|
+
return this.readInto({
|
|
409
|
+
...options, accept: '.yaml,.yml,.json', what: 'the semantic API',
|
|
410
|
+
command: (bytes, name) => {
|
|
411
|
+
const reader = new BianSemanticApiReader({ depth, entityNames: this.model.level('logical').entities.map(e => e.name) });
|
|
412
|
+
read = reader.read(BianSemanticApiReader.parse(new TextDecoder().decode(bytes)));
|
|
413
|
+
const [sd] = read.serviceDomains;
|
|
414
|
+
return this.model.bringCommand(read, `Read ${sd.name} from "${name}"`);
|
|
415
|
+
},
|
|
416
|
+
nothing: (name) => `"${name}" brings nothing the model does not have.`,
|
|
417
|
+
then: () => {
|
|
418
|
+
// The service domain is the one here of its id or name (bringCommand keeps it), else the new one.
|
|
419
|
+
const [sd] = read.serviceDomains;
|
|
420
|
+
const here = this.model.perspective(sd.id) ?? this.model.perspectives.find(p => p.name.toLowerCase() === sd.name.toLowerCase());
|
|
421
|
+
this.chartOptions.level = 'logical';
|
|
422
|
+
this.perspectiveId = here?.id ?? sd.id;
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The Data Model Scorecard (ModelScorecard): each category as the model
|
|
429
|
+
* measures it, a reviewer's score and note, its weight; the total in the
|
|
430
|
+
* title. Saved, the reviews and weights are kept (one undo step).
|
|
431
|
+
*/
|
|
432
|
+
scoreModel() {
|
|
433
|
+
const card = this.model.scored();
|
|
434
|
+
const results = card.results();
|
|
435
|
+
const { score, coverage } = card.total(results);
|
|
436
|
+
const title = score == null ? 'Scorecard: nothing scored yet' : `Scorecard: ${score} of 100 (${coverage}% of the weight scored)`;
|
|
437
|
+
return this._dialog(title, card.fields(results), (values) => {
|
|
438
|
+
this.runModelEdit(this.model.scorecardCommand(ModelScorecard.specFrom(values)));
|
|
439
|
+
}, () => null);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** The classification scheme's dialog (its sensitivity levels in order, its categories); saved, one undo step. */
|
|
443
|
+
editClassificationScheme() {
|
|
444
|
+
return this._dialog('Classification scheme', ClassificationScheme.fields(this.model.classificationScheme), (values) => {
|
|
445
|
+
this.runModelEdit(this.model.classificationSchemeCommand(ClassificationScheme.specFrom(values)));
|
|
446
|
+
}, (values) => (ClassificationScheme.specFrom(values).levels.length ? null : 'Give it at least one level'));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Model ▸ Naming standard ▸: edit it, start from the conventional one, or remove it (Check model… checks names against it). */
|
|
450
|
+
namingItems() {
|
|
451
|
+
const has = !!this.model.namingStandard;
|
|
452
|
+
return [
|
|
453
|
+
{ label: has ? 'Edit…' : 'Set…', action: () => this.editNamingStandard() },
|
|
454
|
+
...(has ? [] : [{ label: 'Use the conventional one', action: () => this.runModelEdit(this.model.namingStandardCommand(NamingStandard.CONVENTIONAL)) }]),
|
|
455
|
+
...(has ? [{ label: 'Remove', action: () => this.runModelEdit(this.model.namingStandardCommand(null)) }] : []),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The naming standard's dialog (NamingStandard.fields: a rule per level for
|
|
461
|
+
* entities and attributes); saved, one undo step. A standard that asks
|
|
462
|
+
* nothing is none.
|
|
463
|
+
*/
|
|
464
|
+
editNamingStandard() {
|
|
465
|
+
return this._dialog('Naming standard', NamingStandard.fields(this.model.namingStandard), (values) => {
|
|
466
|
+
const spec = NamingStandard.specFrom(values);
|
|
467
|
+
const asks = Object.values(spec).some(kinds => Object.values(kinds).some(r => r.style || r.classWords.length || r.maxLength || Object.keys(r.abbreviations).length));
|
|
468
|
+
this.runModelEdit(this.model.namingStandardCommand(asks ? spec : null));
|
|
469
|
+
}, () => null);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Model ▸ Data types ▸: a new one of each kind, then each to change or remove. */
|
|
473
|
+
dataTypeItems() {
|
|
474
|
+
const types = this.model.dataTypes;
|
|
475
|
+
return [
|
|
476
|
+
...[EnumerationType, StructuredType, DataType].map(Kind => ({ label: `New ${Kind.kindLabel.toLowerCase()}…`, action: () => this.editDataType(null, Kind) })),
|
|
477
|
+
...(types.length ? [
|
|
478
|
+
{ separator: true },
|
|
479
|
+
{ label: 'Show data types', action: () => this.setLevel(DataModel.DATA_TYPES) },
|
|
480
|
+
{ label: 'Edit', items: types.map(t => ({ label: `${t.name} (${t.constructor.kindLabel.toLowerCase()})…`, action: () => this.editDataType(t) })) },
|
|
481
|
+
{ label: 'Delete', items: types.map(t => ({ label: t.name, action: () => this.removeDataType(t.id) })) },
|
|
482
|
+
] : []),
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** A dialog making a data type of a kind (none given) or changing one (its kind's fields); applied, the data types are shown. */
|
|
487
|
+
editDataType(type, Kind = type?.constructor ?? DataType) {
|
|
488
|
+
const title = type ? `${Kind.kindLabel}: ${type.name}` : `New ${Kind.kindLabel.toLowerCase()}`;
|
|
489
|
+
return this._dialog(title, Kind.fields(this.model, type), (values) => {
|
|
490
|
+
const spec = Kind.specFrom(values, this.model, type);
|
|
491
|
+
if (type) this.model.updateDataType(type.id, spec); else this.model.addDataType(spec);
|
|
492
|
+
this.chartOptions.level = DataModel.DATA_TYPES;
|
|
493
|
+
this._changed();
|
|
494
|
+
this._redraw();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
removeDataType(id) {
|
|
499
|
+
this.model.removeDataType(id);
|
|
500
|
+
if (this.level === DataModel.DATA_TYPES && !this.model.dataTypes.length) this.chartOptions.level = 'logical';
|
|
501
|
+
this._changed();
|
|
502
|
+
this._redraw();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Check the model (DataModelChecker) and list the findings; choosing one shows it (revealFinding). Returns the findings. */
|
|
506
|
+
checkModel() {
|
|
507
|
+
const findings = this.model.checker().run();
|
|
508
|
+
new ModelCheckDialog({ findings, onReveal: (finding) => this.revealFinding(finding), document: this.document }).open();
|
|
509
|
+
return findings;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Show what a finding names: an entity's level, a service domain seen through at the logical level, the mapping or data types view. */
|
|
513
|
+
revealFinding(finding) {
|
|
514
|
+
if (finding.kind === 'perspective') {
|
|
515
|
+
this.chartOptions.level = 'logical';
|
|
516
|
+
this.setPerspective(finding.ids[0]);
|
|
517
|
+
} else if (finding.kind === 'mapping') {
|
|
518
|
+
this.setLevel(DataModel.MAPPING);
|
|
519
|
+
} else if (finding.kind === 'data-type') {
|
|
520
|
+
this.setLevel(DataModel.DATA_TYPES);
|
|
521
|
+
} else if (finding.level) {
|
|
522
|
+
this.setLevel(finding.level);
|
|
523
|
+
}
|
|
524
|
+
this.menuBar?.render?.();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** A dialog making a perspective (none given) or changing one (DataModelPerspective.fields); applied, it is the one seen through. */
|
|
528
|
+
editPerspective(perspective, Kind = perspective?.constructor ?? DataModelPerspective) {
|
|
529
|
+
const title = perspective ? `${Kind.kindLabel}: ${perspective.name}` : `New ${Kind.kindLabel.toLowerCase()}`;
|
|
530
|
+
return this._dialog(title, Kind.fields(this.model, perspective), (values) => {
|
|
531
|
+
const spec = Kind.specFrom(values, this.model, perspective);
|
|
532
|
+
const saved = perspective ? this.model.updatePerspective(perspective.id, spec) : this.model.addPerspective(spec);
|
|
533
|
+
this._changed();
|
|
534
|
+
this.setPerspective(saved.id);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** A dialog making a context (none given) or changing one (DataModelContext.fields); applied, it is the one read in. */
|
|
539
|
+
editContext(context) {
|
|
540
|
+
return this._dialog(context ? `Context: ${context.name}` : 'New context', DataModelContext.fields(this.model, context), (values) => {
|
|
541
|
+
const spec = DataModelContext.specFrom(values, this.model, context);
|
|
542
|
+
const saved = context ? this.model.updateContext(context.id, spec) : this.model.addContext(spec);
|
|
543
|
+
this._changed();
|
|
544
|
+
this.setContext(saved.id);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
removePerspective(id) {
|
|
549
|
+
this.model.removePerspective(id);
|
|
550
|
+
this._changed();
|
|
551
|
+
this.setPerspective(null);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
removeContext(id) {
|
|
555
|
+
this.model.removeContext(id);
|
|
556
|
+
this._changed();
|
|
557
|
+
this.setContext(null);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Open a form (a name is needed). @private */
|
|
561
|
+
_dialog(title, fields, apply, validate = ({ name }) => ((name ?? '').trim() ? null : 'Give it a name')) {
|
|
562
|
+
return new FormDialog({
|
|
563
|
+
title, fields, submitLabel: 'Save', idPrefix: 'data-model-',
|
|
564
|
+
document: this.container?.ownerDocument ?? globalThis.document,
|
|
565
|
+
validate, onApply: apply,
|
|
566
|
+
}).open();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** The perspectives or contexts changed: the toolbar shows them anew. @private */
|
|
570
|
+
_changed() {
|
|
571
|
+
if (this.menuBar) this.menuBar.items = this.menuBarItems();
|
|
572
|
+
this.menuBar?.render?.();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Export ▸: the picture, as PNG or SVG, named after the model. */
|
|
576
|
+
exportItems() {
|
|
577
|
+
const name = this.model.name || this.title || 'data-model';
|
|
578
|
+
return [
|
|
579
|
+
{ label: 'Image (PNG)', action: () => downloadPng(this.canvasHost, { name }) },
|
|
580
|
+
{ label: 'Image (SVG)', action: () => downloadSvg(this.canvasHost, { name }) },
|
|
581
|
+
{ separator: true },
|
|
582
|
+
{ label: 'SQL DDL (physical level)', action: () => this.exportDdl() },
|
|
583
|
+
];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** The physical level as SQL DDL (SqlDdlFileFormat), downloaded as the model's name. Returns the file. */
|
|
587
|
+
exportDdl({ download = (file, fileName) => downloadBlob(new (this.document.defaultView ?? globalThis).Blob([file.data], { type: file.mimeType }), fileName) } = {}) {
|
|
588
|
+
const file = new SqlDdlFileFormat().export(this.model);
|
|
589
|
+
download(file, `${String(this.model.name || this.title || 'model').replace(/[^A-Za-z0-9._-]+/g, '-')}.sql`);
|
|
590
|
+
return file;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Redraw now (a choice made), when there is somewhere to draw. @private */
|
|
594
|
+
_redraw() {
|
|
595
|
+
if (this.canvasHost) this._draw();
|
|
596
|
+
// Its toolbar shows what is drawn, however it was chosen (a finding, a lineage, a service domain opened).
|
|
597
|
+
this.menuBar?.refresh?.();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Keep where the boxes and notes were dragged (the drawing is made anew each
|
|
602
|
+
* time): on the level shown, or in the mapping view's own layout. Then draw.
|
|
603
|
+
*/
|
|
604
|
+
_draw() {
|
|
605
|
+
if (this._keepPlaces()) this.model.edited(); // a box or note moved is a change, kept and saved
|
|
606
|
+
this._drawnLevel = this.level;
|
|
607
|
+
super._draw();
|
|
608
|
+
this._drawnPlaces = this._placesOnScreen();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Where the drawing has each box and note now, a copy a drag does not change. @private */
|
|
612
|
+
_placesOnScreen() {
|
|
613
|
+
const pos = this.engine?._graph?.pos;
|
|
614
|
+
return pos ? new Map([...pos].map(([id, p]) => [id, { x: p.x, y: p.y }])) : null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Keep where the drawing on screen has its boxes and notes: on the level
|
|
619
|
+
* it shows, or in the mapping view's layout. Returns whether any moved.
|
|
620
|
+
* A box the model has a place for and that was not moved on screen since
|
|
621
|
+
* it was drawn keeps the model's place — the model may have moved it
|
|
622
|
+
* since (laid out, an undo), and the screen still shows where it was. @private
|
|
623
|
+
*/
|
|
624
|
+
_keepPlaces() {
|
|
625
|
+
const pos = this.engine?._graph?.pos;
|
|
626
|
+
if (!pos || !this._drawnLevel) return false;
|
|
627
|
+
let moved = false;
|
|
628
|
+
const place = (p, before, put, id) => {
|
|
629
|
+
if (!p || (before && before.x === p.x && before.y === p.y)) return;
|
|
630
|
+
const drawn = this._drawnPlaces?.get(id);
|
|
631
|
+
if (drawn && drawn.x === p.x && drawn.y === p.y && Number.isFinite(before?.x) && Number.isFinite(before?.y)) return;
|
|
632
|
+
put({ x: p.x, y: p.y });
|
|
633
|
+
moved = true;
|
|
634
|
+
};
|
|
635
|
+
const other = DataModel.otherView(this._drawnLevel);
|
|
636
|
+
if (other && !other.keepsPlaces) return false; // laid out by its structure, anew each time
|
|
637
|
+
if (other) {
|
|
638
|
+
for (const [id, p] of pos) {
|
|
639
|
+
if (id.includes(':')) place(p, this.model.mappingLayout[id], (at) => { this.model.mappingLayout[id] = at; }, id);
|
|
640
|
+
}
|
|
641
|
+
return moved;
|
|
642
|
+
}
|
|
643
|
+
for (const entity of this.model.level(this._drawnLevel).entities) {
|
|
644
|
+
place(pos.get(entity.id), entity, (at) => Object.assign(entity, at), entity.id);
|
|
645
|
+
}
|
|
646
|
+
for (const rule of this.model.contexts.flatMap(c => c.constraints)) {
|
|
647
|
+
const p = pos.get(rule.id);
|
|
648
|
+
const before = this.model.notePlace(this._drawnLevel, rule);
|
|
649
|
+
place(p, Number.isFinite(before.x) ? before : null, (at) => this.model.placeNote(this._drawnLevel, rule, at), rule.id);
|
|
650
|
+
}
|
|
651
|
+
return moved;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
dispose() {
|
|
655
|
+
this.menuBar?.dispose?.();
|
|
656
|
+
super.dispose();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** The spec again: the model (with where its boxes are), what is chosen, and the other options. */
|
|
660
|
+
serialize() {
|
|
661
|
+
const { options, chartType, ...base } = super.serialize();
|
|
662
|
+
const { level, notation, ...rest } = options;
|
|
663
|
+
return {
|
|
664
|
+
...base, ...this.model.toJSON(),
|
|
665
|
+
perspective: this.perspectiveId, context: this.contextId, level, notation, options: rest,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|