@memberjunction/ng-dashboard-viewer 6.2.0-edge.3 → 6.2.0-edge.4

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.
Files changed (99) hide show
  1. package/README.md +165 -17
  2. package/dist/lib/config-dialogs/confirm-dialog.component.d.ts +3 -4
  3. package/dist/lib/config-dialogs/confirm-dialog.component.d.ts.map +1 -1
  4. package/dist/lib/config-dialogs/confirm-dialog.component.js +3 -2
  5. package/dist/lib/config-dialogs/confirm-dialog.component.js.map +1 -1
  6. package/dist/lib/config-panels/artifact-config-panel.component.d.ts.map +1 -1
  7. package/dist/lib/config-panels/artifact-config-panel.component.js +59 -50
  8. package/dist/lib/config-panels/artifact-config-panel.component.js.map +1 -1
  9. package/dist/lib/config-panels/base-config-panel.d.ts +8 -3
  10. package/dist/lib/config-panels/base-config-panel.d.ts.map +1 -1
  11. package/dist/lib/config-panels/base-config-panel.js +10 -3
  12. package/dist/lib/config-panels/base-config-panel.js.map +1 -1
  13. package/dist/lib/config-panels/query-config-panel.component.js +124 -99
  14. package/dist/lib/config-panels/query-config-panel.component.js.map +1 -1
  15. package/dist/lib/config-panels/view-config-panel.component.js +111 -103
  16. package/dist/lib/config-panels/view-config-panel.component.js.map +1 -1
  17. package/dist/lib/config-panels/weburl-config-panel.component.js +89 -80
  18. package/dist/lib/config-panels/weburl-config-panel.component.js.map +1 -1
  19. package/dist/lib/dashboard-browser/dashboard-browser.component.d.ts +79 -28
  20. package/dist/lib/dashboard-browser/dashboard-browser.component.d.ts.map +1 -1
  21. package/dist/lib/dashboard-browser/dashboard-browser.component.js +446 -447
  22. package/dist/lib/dashboard-browser/dashboard-browser.component.js.map +1 -1
  23. package/dist/lib/dashboard-card/dashboard-card.component.d.ts +69 -0
  24. package/dist/lib/dashboard-card/dashboard-card.component.d.ts.map +1 -0
  25. package/dist/lib/dashboard-card/dashboard-card.component.js +281 -0
  26. package/dist/lib/dashboard-card/dashboard-card.component.js.map +1 -0
  27. package/dist/lib/dashboard-card/dashboard-card.helpers.d.ts +18 -0
  28. package/dist/lib/dashboard-card/dashboard-card.helpers.d.ts.map +1 -0
  29. package/dist/lib/dashboard-card/dashboard-card.helpers.js +43 -0
  30. package/dist/lib/dashboard-card/dashboard-card.helpers.js.map +1 -0
  31. package/dist/lib/dashboard-card/layout-preview-cache.d.ts +15 -0
  32. package/dist/lib/dashboard-card/layout-preview-cache.d.ts.map +1 -0
  33. package/dist/lib/dashboard-card/layout-preview-cache.js +22 -0
  34. package/dist/lib/dashboard-card/layout-preview-cache.js.map +1 -0
  35. package/dist/lib/dashboard-viewer/dashboard-viewer.component.d.ts +69 -12
  36. package/dist/lib/dashboard-viewer/dashboard-viewer.component.d.ts.map +1 -1
  37. package/dist/lib/dashboard-viewer/dashboard-viewer.component.js +173 -145
  38. package/dist/lib/dashboard-viewer/dashboard-viewer.component.js.map +1 -1
  39. package/dist/lib/dashboard-viewer.module.d.ts +24 -20
  40. package/dist/lib/dashboard-viewer.module.d.ts.map +1 -1
  41. package/dist/lib/dashboard-viewer.module.js +42 -5
  42. package/dist/lib/dashboard-viewer.module.js.map +1 -1
  43. package/dist/lib/dialogs/add-panel-dialog/add-panel-dialog.component.d.ts +7 -12
  44. package/dist/lib/dialogs/add-panel-dialog/add-panel-dialog.component.d.ts.map +1 -1
  45. package/dist/lib/dialogs/add-panel-dialog/add-panel-dialog.component.js +3 -1
  46. package/dist/lib/dialogs/add-panel-dialog/add-panel-dialog.component.js.map +1 -1
  47. package/dist/lib/dialogs/dashboard-name-dialog/dashboard-name-dialog.component.d.ts +54 -0
  48. package/dist/lib/dialogs/dashboard-name-dialog/dashboard-name-dialog.component.d.ts.map +1 -0
  49. package/dist/lib/dialogs/dashboard-name-dialog/dashboard-name-dialog.component.js +156 -0
  50. package/dist/lib/dialogs/dashboard-name-dialog/dashboard-name-dialog.component.js.map +1 -0
  51. package/dist/lib/dialogs/edit-part-dialog/edit-part-dialog.component.d.ts +5 -11
  52. package/dist/lib/dialogs/edit-part-dialog/edit-part-dialog.component.d.ts.map +1 -1
  53. package/dist/lib/dialogs/edit-part-dialog/edit-part-dialog.component.js +3 -1
  54. package/dist/lib/dialogs/edit-part-dialog/edit-part-dialog.component.js.map +1 -1
  55. package/dist/lib/dialogs/part-dialog/dashboard-part-dialog.component.d.ts +147 -0
  56. package/dist/lib/dialogs/part-dialog/dashboard-part-dialog.component.d.ts.map +1 -0
  57. package/dist/lib/dialogs/part-dialog/dashboard-part-dialog.component.js +536 -0
  58. package/dist/lib/dialogs/part-dialog/dashboard-part-dialog.component.js.map +1 -0
  59. package/dist/lib/layout-preview/dashboard-layout-preview-node.component.d.ts +20 -0
  60. package/dist/lib/layout-preview/dashboard-layout-preview-node.component.d.ts.map +1 -0
  61. package/dist/lib/layout-preview/dashboard-layout-preview-node.component.js +80 -0
  62. package/dist/lib/layout-preview/dashboard-layout-preview-node.component.js.map +1 -0
  63. package/dist/lib/layout-preview/dashboard-layout-preview.component.d.ts +28 -0
  64. package/dist/lib/layout-preview/dashboard-layout-preview.component.d.ts.map +1 -0
  65. package/dist/lib/layout-preview/dashboard-layout-preview.component.js +62 -0
  66. package/dist/lib/layout-preview/dashboard-layout-preview.component.js.map +1 -0
  67. package/dist/lib/layout-preview/dashboard-layout-preview.d.ts +33 -0
  68. package/dist/lib/layout-preview/dashboard-layout-preview.d.ts.map +1 -0
  69. package/dist/lib/layout-preview/dashboard-layout-preview.js +138 -0
  70. package/dist/lib/layout-preview/dashboard-layout-preview.js.map +1 -0
  71. package/dist/lib/models/dashboard-layout-editor.d.ts +78 -0
  72. package/dist/lib/models/dashboard-layout-editor.d.ts.map +1 -0
  73. package/dist/lib/models/dashboard-layout-editor.js +374 -0
  74. package/dist/lib/models/dashboard-layout-editor.js.map +1 -0
  75. package/dist/lib/models/remove-part-confirm.d.ts +8 -0
  76. package/dist/lib/models/remove-part-confirm.d.ts.map +1 -0
  77. package/dist/lib/models/remove-part-confirm.js +23 -0
  78. package/dist/lib/models/remove-part-confirm.js.map +1 -0
  79. package/dist/lib/parts/artifact-part.component.js +2 -2
  80. package/dist/lib/parts/artifact-part.component.js.map +1 -1
  81. package/dist/lib/parts/query-part.component.js +2 -2
  82. package/dist/lib/parts/query-part.component.js.map +1 -1
  83. package/dist/lib/parts/view-part.component.js +2 -2
  84. package/dist/lib/parts/view-part.component.js.map +1 -1
  85. package/dist/lib/parts/weburl-part.component.js +2 -2
  86. package/dist/lib/parts/weburl-part.component.js.map +1 -1
  87. package/dist/lib/services/golden-layout-wrapper.service.d.ts +40 -0
  88. package/dist/lib/services/golden-layout-wrapper.service.d.ts.map +1 -1
  89. package/dist/lib/services/golden-layout-wrapper.service.js +117 -9
  90. package/dist/lib/services/golden-layout-wrapper.service.js.map +1 -1
  91. package/dist/lib/services/stack-part-actions.d.ts +11 -0
  92. package/dist/lib/services/stack-part-actions.d.ts.map +1 -0
  93. package/dist/lib/services/stack-part-actions.js +41 -0
  94. package/dist/lib/services/stack-part-actions.js.map +1 -0
  95. package/dist/public-api.d.ts +9 -0
  96. package/dist/public-api.d.ts.map +1 -1
  97. package/dist/public-api.js +13 -0
  98. package/dist/public-api.js.map +1 -1
  99. package/package.json +13 -13
@@ -6,6 +6,7 @@ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
6
6
  import { DashboardEngine } from '@memberjunction/core-entities';
7
7
  import { CreateDefaultDashboardConfig, GeneratePanelId, ExtractPanelsFromLayout, FindPanelInLayout } from '../models/dashboard-types';
8
8
  import { GoldenLayoutWrapperService } from '../services/golden-layout-wrapper.service';
9
+ import { InsertPanel, FindPanelPath, DescribePath } from '../models/dashboard-layout-editor';
9
10
  import { BaseDashboardPart } from '../parts/base-dashboard-part';
10
11
  import * as i0 from "@angular/core";
11
12
  import * as i1 from "@memberjunction/ng-shared-generic";
@@ -110,13 +111,15 @@ function DashboardViewerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1)
110
111
  function DashboardViewerComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
111
112
  const _r7 = i0.ɵɵgetCurrentView();
112
113
  i0.ɵɵelementStart(0, "mj-empty-state", 27);
113
- i0.ɵɵlistener("Action", function DashboardViewerComponent_Conditional_6_Template_mj_empty_state_Action_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onAddPanelClick()); });
114
+ i0.ɵɵlistener("Action", function DashboardViewerComponent_Conditional_6_Template_mj_empty_state_Action_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnAddPanelClick()); });
114
115
  i0.ɵɵelementEnd();
115
116
  } if (rf & 2) {
116
117
  const ctx_r1 = i0.ɵɵnextContext();
117
- i0.ɵɵproperty("ActionText", ctx_r1.isEditing ? "Add Your First Part" : "");
118
+ i0.ɵɵproperty("ActionText", ctx_r1.IsEditing || ctx_r1.CanEdit ? "Add part" : "");
118
119
  } }
119
120
  const LAYOUT_CONTAINER_SIZE_TIMEOUT_MS = 10_000;
121
+ /** Time a layout build waits after its parts are created, before its final size update. */
122
+ const LAYOUT_SETTLE_DELAY_MS = 100;
120
123
  /**
121
124
  * Main dashboard viewer component.
122
125
  * Renders a configurable dashboard with draggable/resizable panels using Golden Layout.
@@ -179,7 +182,6 @@ export class DashboardViewerComponent extends BaseAngularComponent {
179
182
  this._isEditing = value;
180
183
  // When isEditing changes (and layout exists), reinitialize to apply GL settings
181
184
  if (value !== previous && this._glService) {
182
- console.log('[DashboardViewer] isEditing changed from', previous, 'to', value);
183
185
  this.updatePanelEditModes();
184
186
  }
185
187
  }
@@ -194,6 +196,11 @@ export class DashboardViewerComponent extends BaseAngularComponent {
194
196
  set isEditing(value) {
195
197
  this.IsEditing = value;
196
198
  }
199
+ /**
200
+ * Whether the user may edit this dashboard. An empty dashboard then offers Add part in view mode
201
+ * too; the host decides how to get into edit mode.
202
+ */
203
+ CanEdit = false;
197
204
  /** Whether to show the toolbar */
198
205
  _showToolbar = true;
199
206
  set ShowToolbar(value) {
@@ -405,6 +412,8 @@ export class DashboardViewerComponent extends BaseAngularComponent {
405
412
  set hasUnsavedChanges(value) {
406
413
  this.HasUnsavedChanges = value;
407
414
  }
415
+ /** The saved configuration (UIConfigDetails) the layout was loaded from or last saved as. */
416
+ _loadedConfigDetails = null;
408
417
  /**
409
418
  * Helper to check if layout has any panels (for template use).
410
419
  * Panels are stored in componentState within the layout tree.
@@ -425,7 +434,8 @@ export class DashboardViewerComponent extends BaseAngularComponent {
425
434
  _resolveLayoutReady = null;
426
435
  _rejectLayoutReady = null;
427
436
  _layoutInitGeneration = 0;
428
- _layoutReadyTimer = null;
437
+ /** Number of the latest layout build. A build that is no longer the latest stops at its next step. */
438
+ _layoutBuild = 0;
429
439
  _resolveDeferredLayoutInit = null;
430
440
  _rejectDeferredLayoutInit = null;
431
441
  _deferredLayoutInitTimer = null;
@@ -450,7 +460,6 @@ export class DashboardViewerComponent extends BaseAngularComponent {
450
460
  const generation = this._layoutInitGeneration;
451
461
  this.resolveLayoutReady(generation);
452
462
  this._layoutInitGeneration++;
453
- this.clearLayoutReadyTimer();
454
463
  this.cancelDeferredLayoutInit();
455
464
  this.destroyLayout();
456
465
  }
@@ -470,17 +479,19 @@ export class DashboardViewerComponent extends BaseAngularComponent {
470
479
  return this.ToggleEditMode();
471
480
  }
472
481
  /**
473
- * Add a new panel to the dashboard.
474
- * The panel is stored in GL's componentState - no separate panels array.
482
+ * Adds a panel and returns its id, or null when nothing was added (no layout yet, or an unknown
483
+ * part type). With a location, the layout editor places the panel there and the layout is
484
+ * rebuilt; without one, Golden Layout adds the panel to its first stack.
485
+ * @throws LayoutEditError when the location is not valid or names a panel that is not in the layout.
475
486
  */
476
487
  async AddPanel(partTypeId, panelConfig, title, icon, location) {
477
488
  if (!this.config || !this._glService) {
478
- return;
489
+ return null;
479
490
  }
480
491
  const partType = this.PartTypes.find(pt => UUIDsEqual(pt.ID, partTypeId));
481
492
  if (!partType) {
482
493
  this.error.emit({ message: `Unknown panel type: ${partTypeId}` });
483
- return;
494
+ return null;
484
495
  }
485
496
  const panel = {
486
497
  id: GeneratePanelId(),
@@ -489,15 +500,19 @@ export class DashboardViewerComponent extends BaseAngularComponent {
489
500
  icon: icon || partType.Icon || 'fa-solid fa-window-maximize',
490
501
  config: panelConfig
491
502
  };
503
+ if (location) {
504
+ await this.ApplyLayout(InsertPanel(this._glService.getLayoutConfig() ?? this.config.layout, panel, location));
505
+ return panel.id;
506
+ }
492
507
  // Add to Golden Layout - panel data stored in componentState
493
- this._glService.addPanel(panel, location);
508
+ this._glService.addPanel(panel);
494
509
  // Sync the layout config from Golden Layout to capture the new panel
495
- // The layout IS the source of truth - it contains the panel in componentState
496
510
  const currentLayout = this._glService.getLayoutConfig();
497
511
  if (currentLayout) {
498
512
  this.config.layout = currentLayout;
499
513
  }
500
514
  this.markDirty();
515
+ return panel.id;
501
516
  }
502
517
  /** @deprecated Use {@link AddPanel}. */
503
518
  async addPanel(partTypeId, panelConfig, title, icon, location) {
@@ -546,6 +561,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
546
561
  const saved = await this._dashboard.Save();
547
562
  if (saved) {
548
563
  this.HasUnsavedChanges = false;
564
+ this._loadedConfigDetails = this._dashboard.UIConfigDetails ?? null;
549
565
  this.DashboardSaved.emit(this._dashboard);
550
566
  }
551
567
  return saved;
@@ -573,6 +589,32 @@ export class DashboardViewerComponent extends BaseAngularComponent {
573
589
  async refreshAllPanels() {
574
590
  return this.RefreshAllPanels();
575
591
  }
592
+ /**
593
+ * Shows `layout`: stores it as the dashboard's layout, rebuilds the panels, marks the dashboard
594
+ * changed and emits a `layout` config change. Build the layout with the layout editor functions.
595
+ * Rejects when the rebuild fails, for example when the container stays at zero size.
596
+ */
597
+ async ApplyLayout(layout) {
598
+ if (!this.config)
599
+ return;
600
+ this.config.layout = layout;
601
+ try {
602
+ if (this._glService) {
603
+ await this.initializeLayout();
604
+ }
605
+ }
606
+ finally {
607
+ // After the rebuild, so an auto-save reads the new layout back from Golden Layout.
608
+ // The layout has changed even when the rebuild fails.
609
+ this.markDirty();
610
+ }
611
+ this.configChanged.emit({ config: this.config, changeType: 'layout' });
612
+ }
613
+ /** The panel's address in the layout, for example `row/1 › tab 0`, or null when it is not in the layout. */
614
+ GetPanelPath(panelId) {
615
+ const path = FindPanelPath(this.config?.layout ?? null, panelId);
616
+ return path ? DescribePath(path) : null;
617
+ }
576
618
  /**
577
619
  * Resolves when the current dashboard's Golden Layout instance has initialized
578
620
  * against a non-zero container and completed its first size update.
@@ -584,6 +626,42 @@ export class DashboardViewerComponent extends BaseAngularComponent {
584
626
  waitForLayoutReady() {
585
627
  return this.WaitForLayoutReady();
586
628
  }
629
+ /**
630
+ * True when the dashboard's saved layout differs from the one this viewer last loaded or saved,
631
+ * for example after another tab saved the dashboard.
632
+ * @param dashboard A saved copy of the shown dashboard. Defaults to the shown dashboard.
633
+ */
634
+ HasNewerSavedLayout(dashboard = this._dashboard) {
635
+ return !!dashboard && (dashboard.UIConfigDetails ?? null) !== this._loadedConfigDetails;
636
+ }
637
+ /**
638
+ * Shows the dashboard's saved layout again and drops unsaved layout changes. The current layout
639
+ * is removed at once, so an edit-mode change made right after the call applies to the new
640
+ * layout. A hidden viewer builds the layout when its container gets a size, with no time limit.
641
+ * @param dashboard A saved copy of the shown dashboard to show instead. Defaults to the shown dashboard.
642
+ */
643
+ ReloadFromSaved(dashboard = this._dashboard) {
644
+ if (!dashboard) {
645
+ return Promise.resolve();
646
+ }
647
+ this._dashboard = dashboard;
648
+ this.HasUnsavedChanges = false;
649
+ this.destroyLayout();
650
+ return this.onDashboardChanged(false);
651
+ }
652
+ /**
653
+ * Takes another copy of the shown dashboard without rebuilding the layout, for example after
654
+ * DashboardEngine reloaded its dashboards. Later saves write that copy. Returns false, and changes
655
+ * nothing, when the copy is of another dashboard or its saved layout differs from the one this
656
+ * viewer last loaded or saved (use ReloadFromSaved then).
657
+ */
658
+ UseSavedCopy(dashboard) {
659
+ if (!this._dashboard || !UUIDsEqual(dashboard.ID, this._dashboard.ID) || this.HasNewerSavedLayout(dashboard)) {
660
+ return false;
661
+ }
662
+ this._dashboard = dashboard;
663
+ return true;
664
+ }
587
665
  /**
588
666
  * Get the current configuration
589
667
  */
@@ -628,8 +706,9 @@ export class DashboardViewerComponent extends BaseAngularComponent {
628
706
  * Update a panel's configuration.
629
707
  * Since panels live in componentState within the layout, we need to
630
708
  * update the layout tree directly or reinitialize with updated data.
709
+ * Resolves once the layout is rebuilt with the updated panel; rejects when the rebuild fails.
631
710
  */
632
- UpdatePanelConfig(panelId, newConfig, title, icon) {
711
+ async UpdatePanelConfig(panelId, newConfig, title, icon) {
633
712
  if (!this.config || !this._glService)
634
713
  return;
635
714
  // Get current layout which contains all panel data
@@ -641,7 +720,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
641
720
  this.config.layout = currentLayout;
642
721
  this.markDirty();
643
722
  // Reinitialize layout to reflect the updated panel
644
- this.initializeLayout();
723
+ await this.initializeLayout();
645
724
  }
646
725
  /** @deprecated Use {@link UpdatePanelConfig}. */
647
726
  updatePanelConfig(panelId, newConfig, title, icon) {
@@ -696,7 +775,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
696
775
  return this.OnAddPanelClick();
697
776
  }
698
777
  /**
699
- * Handle "Open in Tab" button click - emits event for parent to open dashboard in its own tab
778
+ * Handle "Open in Tab" button click - emits event for the parent to open the dashboard in a dashboard tab
700
779
  */
701
780
  OnOpenInTabClick() {
702
781
  if (this._dashboard) {
@@ -756,13 +835,19 @@ export class DashboardViewerComponent extends BaseAngularComponent {
756
835
  this.cdr.detectChanges();
757
836
  }
758
837
  }
759
- async onDashboardChanged() {
838
+ /**
839
+ * Builds the layout from the dashboard's saved configuration.
840
+ * @param limitSizeWait When true, a container that stays at zero size fails the build after
841
+ * LAYOUT_CONTAINER_SIZE_TIMEOUT_MS. When false, the build waits until the container gets a size.
842
+ */
843
+ async onDashboardChanged(limitSizeWait = true) {
760
844
  if (!this._dashboard)
761
845
  return;
762
846
  const generation = this.startLayoutReadyCycle();
763
847
  try {
764
848
  // Parse or create config
765
849
  this.config = this.parseOrCreateConfig();
850
+ this._loadedConfigDetails = this._dashboard.UIConfigDetails ?? null;
766
851
  // Wait for part types to be loaded before initializing layout
767
852
  // This ensures partTypes array is populated when createPanelComponent is called
768
853
  if (this._partTypesLoaded) {
@@ -773,7 +858,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
773
858
  return;
774
859
  }
775
860
  // Initialize layout
776
- await this.initializeLayout(generation);
861
+ await this.initializeLayout(generation, limitSizeWait);
777
862
  }
778
863
  catch (err) {
779
864
  const error = err instanceof Error ? err : new Error(String(err));
@@ -803,10 +888,11 @@ export class DashboardViewerComponent extends BaseAngularComponent {
803
888
  // ========================================
804
889
  // Private Methods - Layout
805
890
  // ========================================
806
- async initializeLayout(generation = this._layoutInitGeneration) {
891
+ async initializeLayout(generation = this._layoutInitGeneration, limitSizeWait = true) {
807
892
  if (!this.config || !this.LayoutContainer?.nativeElement) {
808
893
  return;
809
894
  }
895
+ const build = ++this._layoutBuild;
810
896
  const el = this.LayoutContainer.nativeElement;
811
897
  const rect = el.getBoundingClientRect();
812
898
  if (rect.width === 0 || rect.height === 0) {
@@ -819,8 +905,8 @@ export class DashboardViewerComponent extends BaseAngularComponent {
819
905
  const reason = 'layout container has zero size';
820
906
  this.emitLayoutLifecycle('waiting-for-size', reason);
821
907
  this.LayoutDeferred.emit({ reason });
822
- await this.waitForLayoutContainerSize(el, generation);
823
- if (generation !== this._layoutInitGeneration) {
908
+ await this.waitForLayoutContainerSize(el, generation, limitSizeWait);
909
+ if (!this.isCurrentLayoutBuild(build, generation)) {
824
910
  return;
825
911
  }
826
912
  }
@@ -830,14 +916,15 @@ export class DashboardViewerComponent extends BaseAngularComponent {
830
916
  // Destroy existing layout
831
917
  this.destroyLayout();
832
918
  // Create new Golden Layout service
833
- this._glService = new GoldenLayoutWrapperService();
919
+ const glService = new GoldenLayoutWrapperService();
920
+ this._glService = glService;
834
921
  // Subscribe to layout events
835
922
  this.subscribeToLayoutEvents();
836
923
  const pendingPanelCreations = [];
837
924
  // Panel factory - called by GL when it binds a component
838
925
  // The panel comes directly from GL's componentState (single source of truth)
839
926
  const panelFactory = (panel, container) => {
840
- const creation = this.createPanelComponent(panel, container);
927
+ const creation = this.createPanelComponent(panel, container, glService);
841
928
  pendingPanelCreations.push(creation.catch(error => {
842
929
  console.error('[DashboardViewer] Failed to create panel component:', error);
843
930
  }));
@@ -854,14 +941,8 @@ export class DashboardViewerComponent extends BaseAngularComponent {
854
941
  // However, Angular needs time to complete change detection and render
855
942
  // the dynamic components. A single delayed updateSize() ensures GL
856
943
  // recalculates dimensions after Angular has finished rendering.
857
- await new Promise(resolve => {
858
- this.clearLayoutReadyTimer();
859
- this._layoutReadyTimer = setTimeout(() => {
860
- this._layoutReadyTimer = null;
861
- resolve();
862
- }, 100);
863
- });
864
- if (generation !== this._layoutInitGeneration) {
944
+ await this.waitForLayoutToSettle();
945
+ if (!this.isCurrentLayoutBuild(build, generation)) {
865
946
  return;
866
947
  }
867
948
  this._glService?.updateSize();
@@ -877,8 +958,9 @@ export class DashboardViewerComponent extends BaseAngularComponent {
877
958
  * (re)initialize. Used when initializeLayout() is called while the container is
878
959
  * still 0×0 — e.g. during a cached-component reattach on browser back/forward —
879
960
  * where GoldenLayout would otherwise bind panels that never receive a 'show' event.
961
+ * With `limitSizeWait`, the wait fails after LAYOUT_CONTAINER_SIZE_TIMEOUT_MS.
880
962
  */
881
- waitForLayoutContainerSize(el, generation) {
963
+ waitForLayoutContainerSize(el, generation, limitSizeWait = true) {
882
964
  this.cancelDeferredLayoutInit();
883
965
  return new Promise((resolve, reject) => {
884
966
  this._resolveDeferredLayoutInit = resolve;
@@ -900,10 +982,12 @@ export class DashboardViewerComponent extends BaseAngularComponent {
900
982
  resolve();
901
983
  return;
902
984
  }
903
- this._deferredLayoutInitTimer = setTimeout(() => {
904
- const error = new Error(`Dashboard layout container stayed at zero size for ${LAYOUT_CONTAINER_SIZE_TIMEOUT_MS}ms`);
905
- this.failDeferredLayoutInit(error);
906
- }, LAYOUT_CONTAINER_SIZE_TIMEOUT_MS);
985
+ if (limitSizeWait) {
986
+ this._deferredLayoutInitTimer = setTimeout(() => {
987
+ const error = new Error(`Dashboard layout container stayed at zero size for ${LAYOUT_CONTAINER_SIZE_TIMEOUT_MS}ms`);
988
+ this.failDeferredLayoutInit(error);
989
+ }, LAYOUT_CONTAINER_SIZE_TIMEOUT_MS);
990
+ }
907
991
  ro.observe(el);
908
992
  this._layoutSizeObserver = ro;
909
993
  });
@@ -938,9 +1022,19 @@ export class DashboardViewerComponent extends BaseAngularComponent {
938
1022
  this._deferredLayoutInitTimer = null;
939
1023
  }
940
1024
  }
1025
+ /** True while the build is the latest layout build and its layout cycle is still current. */
1026
+ isCurrentLayoutBuild(build, generation) {
1027
+ return build === this._layoutBuild && generation === this._layoutInitGeneration;
1028
+ }
1029
+ /**
1030
+ * Resolves after LAYOUT_SETTLE_DELAY_MS. Each build waits on its own timer; a build that a newer
1031
+ * build replaced stops after the wait.
1032
+ */
1033
+ waitForLayoutToSettle() {
1034
+ return new Promise(resolve => setTimeout(resolve, LAYOUT_SETTLE_DELAY_MS));
1035
+ }
941
1036
  startLayoutReadyCycle() {
942
1037
  const generation = ++this._layoutInitGeneration;
943
- this.clearLayoutReadyTimer();
944
1038
  this.cancelDeferredLayoutInit();
945
1039
  this._resolveLayoutReady?.();
946
1040
  this._layoutReadyPromise = new Promise((resolve, reject) => {
@@ -967,12 +1061,6 @@ export class DashboardViewerComponent extends BaseAngularComponent {
967
1061
  this._resolveLayoutReady = null;
968
1062
  this._rejectLayoutReady = null;
969
1063
  }
970
- clearLayoutReadyTimer() {
971
- if (this._layoutReadyTimer) {
972
- clearTimeout(this._layoutReadyTimer);
973
- this._layoutReadyTimer = null;
974
- }
975
- }
976
1064
  emitLayoutLifecycle(state, reason, error) {
977
1065
  this.LayoutLifecycle.emit({
978
1066
  state,
@@ -1016,6 +1104,20 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1016
1104
  .subscribe((panelId) => {
1017
1105
  this.onPanelSelected(panelId);
1018
1106
  });
1107
+ this._glService.OnStackAction
1108
+ .pipe(takeUntil(this._destroy$))
1109
+ .subscribe((event) => this.onStackAction(event));
1110
+ }
1111
+ /** Asks the host to edit or remove the part a stack header button acts on. */
1112
+ onStackAction(event) {
1113
+ switch (event.Action) {
1114
+ case 'edit':
1115
+ this.onConfigurePart(event.PanelId);
1116
+ break;
1117
+ case 'remove':
1118
+ this.onRemovePart(event.PanelId);
1119
+ break;
1120
+ }
1019
1121
  }
1020
1122
  onLayoutChanged(event) {
1021
1123
  if (this.config) {
@@ -1054,24 +1156,27 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1054
1156
  /**
1055
1157
  * Create a panel component from the DashboardPanel data.
1056
1158
  * Panel comes directly from GL's componentState - no lookup needed.
1159
+ * @param glService The layout that asks for the part. When it is no longer the viewer's layout
1160
+ * once the part is created (a newer build replaced it, or it was removed), the part is destroyed.
1057
1161
  */
1058
- async createPanelComponent(panel, container) {
1162
+ async createPanelComponent(panel, container, glService) {
1059
1163
  const partType = this.PartTypes.find(pt => UUIDsEqual(pt.ID, panel.partTypeId));
1060
- // Create the panel wrapper with header and content
1164
+ // Create the panel wrapper; the Golden Layout tab shows the part's title
1061
1165
  const wrapper = document.createElement('div');
1062
1166
  wrapper.className = 'dashboard-part-wrapper';
1063
1167
  wrapper.style.cssText = 'display: flex; flex-direction: column; height: 100%; background: var(--mj-bg-surface);';
1064
- // Only show header in edit mode - GL tabs already display the title in view mode
1065
- if (this.IsEditing) {
1066
- const header = this.createPartHeader(panel, panel.id);
1067
- wrapper.appendChild(header);
1068
- }
1069
1168
  // Create content area
1070
1169
  const content = document.createElement('div');
1071
1170
  content.className = 'dashboard-part-content';
1072
1171
  content.style.cssText = 'flex: 1; overflow: auto; min-height: 0;';
1073
1172
  // Try to create dynamic component via ClassFactory
1074
1173
  const componentRef = await this.createDynamicPartComponent(panel, partType, content);
1174
+ if (glService !== this._glService) {
1175
+ if (componentRef) {
1176
+ this.releasePartComponent(componentRef);
1177
+ }
1178
+ return;
1179
+ }
1075
1180
  if (!componentRef) {
1076
1181
  // Fallback to static rendering if no DriverClass or component creation failed
1077
1182
  this.renderPartContent(panel, content, partType);
@@ -1128,90 +1233,6 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1128
1233
  return null;
1129
1234
  }
1130
1235
  }
1131
- createPartHeader(panel, panelId) {
1132
- const header = document.createElement('div');
1133
- header.className = 'dashboard-part-header';
1134
- header.style.cssText = `
1135
- display: flex;
1136
- align-items: center;
1137
- gap: 8px;
1138
- padding: 10px 12px;
1139
- background: var(--mj-bg-surface-card);
1140
- border-bottom: 1px solid var(--mj-border-default);
1141
- min-height: 40px;
1142
- `;
1143
- // Icon and title
1144
- const titleSection = document.createElement('div');
1145
- titleSection.style.cssText = 'display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;';
1146
- titleSection.innerHTML = `
1147
- <i class="${EscapeHTML(panel.icon || 'fa-solid fa-puzzle-piece')}" style="color: var(--mj-brand-primary); font-size: 14px;"></i>
1148
- <span style="font-weight: 500; font-size: 14px; color: var(--mj-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${EscapeHTML(panel.title)}</span>
1149
- `;
1150
- header.appendChild(titleSection);
1151
- // Action buttons (only in edit mode)
1152
- if (this.IsEditing) {
1153
- const actions = document.createElement('div');
1154
- actions.style.cssText = 'display: flex; gap: 4px;';
1155
- // Configure button
1156
- const configBtn = document.createElement('button');
1157
- configBtn.className = 'part-action-btn';
1158
- configBtn.title = 'Configure';
1159
- configBtn.style.cssText = `
1160
- width: 28px;
1161
- height: 28px;
1162
- display: flex;
1163
- align-items: center;
1164
- justify-content: center;
1165
- border: none;
1166
- border-radius: 4px;
1167
- background: transparent;
1168
- color: var(--mj-text-secondary);
1169
- cursor: pointer;
1170
- transition: all 0.15s;
1171
- `;
1172
- configBtn.innerHTML = '<i class="fa-solid fa-cog" style="font-size: 12px;"></i>';
1173
- configBtn.addEventListener('click', () => this.onConfigurePart(panelId));
1174
- configBtn.addEventListener('mouseenter', () => {
1175
- configBtn.style.background = 'var(--mj-border-default)';
1176
- configBtn.style.color = 'var(--mj-text-primary)';
1177
- });
1178
- configBtn.addEventListener('mouseleave', () => {
1179
- configBtn.style.background = 'transparent';
1180
- configBtn.style.color = 'var(--mj-text-secondary)';
1181
- });
1182
- // Remove button
1183
- const removeBtn = document.createElement('button');
1184
- removeBtn.className = 'part-action-btn';
1185
- removeBtn.title = 'Remove';
1186
- removeBtn.style.cssText = `
1187
- width: 28px;
1188
- height: 28px;
1189
- display: flex;
1190
- align-items: center;
1191
- justify-content: center;
1192
- border: none;
1193
- border-radius: 4px;
1194
- background: transparent;
1195
- color: var(--mj-text-secondary);
1196
- cursor: pointer;
1197
- transition: all 0.15s;
1198
- `;
1199
- removeBtn.innerHTML = '<i class="fa-solid fa-times" style="font-size: 12px;"></i>';
1200
- removeBtn.addEventListener('click', () => this.onRemovePart(panelId));
1201
- removeBtn.addEventListener('mouseenter', () => {
1202
- removeBtn.style.background = 'color-mix(in srgb, var(--mj-status-error) 10%, transparent)';
1203
- removeBtn.style.color = 'var(--mj-status-error)';
1204
- });
1205
- removeBtn.addEventListener('mouseleave', () => {
1206
- removeBtn.style.background = 'transparent';
1207
- removeBtn.style.color = 'var(--mj-text-secondary)';
1208
- });
1209
- actions.appendChild(configBtn);
1210
- actions.appendChild(removeBtn);
1211
- header.appendChild(actions);
1212
- }
1213
- return header;
1214
- }
1215
1236
  renderPartContent(panel, container, partType) {
1216
1237
  const config = panel.config;
1217
1238
  switch (config?.type) {
@@ -1238,7 +1259,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1238
1259
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--mj-text-secondary); text-align: center; padding: 24px;">
1239
1260
  <i class="fa-solid fa-globe" style="font-size: 48px; color: var(--mj-text-muted); margin-bottom: 16px;"></i>
1240
1261
  <h4 style="margin: 0 0 8px 0; color: var(--mj-text-primary);">No URL Configured</h4>
1241
- <p style="margin: 0; font-size: 13px;">Click the configure button to set a URL for this part.</p>
1262
+ <p style="margin: 0; font-size: 13px;">Use Edit part to set a URL.</p>
1242
1263
  </div>
1243
1264
  `;
1244
1265
  return;
@@ -1270,7 +1291,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1270
1291
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--mj-text-secondary); text-align: center; padding: 24px;">
1271
1292
  <i class="fa-solid fa-table" style="font-size: 48px; color: var(--mj-text-muted); margin-bottom: 16px;"></i>
1272
1293
  <h4 style="margin: 0 0 8px 0; color: var(--mj-text-primary);">No View Selected</h4>
1273
- <p style="margin: 0; font-size: 13px;">Click configure to select a view for this part.</p>
1294
+ <p style="margin: 0; font-size: 13px;">Use Edit part to choose a view.</p>
1274
1295
  </div>
1275
1296
  `;
1276
1297
  return;
@@ -1306,7 +1327,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1306
1327
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--mj-text-secondary); text-align: center; padding: 24px;">
1307
1328
  <i class="fa-solid fa-database" style="font-size: 48px; color: var(--mj-text-muted); margin-bottom: 16px;"></i>
1308
1329
  <h4 style="margin: 0 0 8px 0; color: var(--mj-text-primary);">No Query Selected</h4>
1309
- <p style="margin: 0; font-size: 13px;">Click configure to select a query for this part.</p>
1330
+ <p style="margin: 0; font-size: 13px;">Use Edit part to choose a query.</p>
1310
1331
  </div>
1311
1332
  `;
1312
1333
  return;
@@ -1340,7 +1361,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1340
1361
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--mj-text-secondary); text-align: center; padding: 24px;">
1341
1362
  <i class="fa-solid fa-cube" style="font-size: 48px; color: var(--mj-text-muted); margin-bottom: 16px;"></i>
1342
1363
  <h4 style="margin: 0 0 8px 0; color: var(--mj-text-primary);">No Artifact Selected</h4>
1343
- <p style="margin: 0; font-size: 13px;">Click configure to select an artifact for this part.</p>
1364
+ <p style="margin: 0; font-size: 13px;">Use Edit part to choose an artifact.</p>
1344
1365
  </div>
1345
1366
  `;
1346
1367
  return;
@@ -1387,14 +1408,15 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1387
1408
  });
1388
1409
  }
1389
1410
  onRemovePart(panelId) {
1390
- // Emit event for parent to show confirmation dialog
1411
+ // Emit event for parent to show confirmation dialog, with the part's title and part type name
1391
1412
  const panel = FindPanelInLayout(this.config?.layout ?? null, panelId);
1392
1413
  this.PanelInteraction.emit({
1393
1414
  panelId,
1394
1415
  interactionType: 'custom',
1395
1416
  payload: {
1396
1417
  action: 'remove-part-requested',
1397
- panelTitle: panel?.title || 'this part'
1418
+ panelTitle: panel?.title || 'this part',
1419
+ partTypeName: this.GetPartTypeForPanel(panelId)?.Name ?? null
1398
1420
  }
1399
1421
  });
1400
1422
  }
@@ -1413,12 +1435,16 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1413
1435
  if (entry) {
1414
1436
  // Destroy the Angular component if present
1415
1437
  if (entry.componentRef) {
1416
- this.appRef.detachView(entry.componentRef.hostView);
1417
- entry.componentRef.destroy();
1438
+ this.releasePartComponent(entry.componentRef);
1418
1439
  }
1419
1440
  this._panelComponents.delete(panelId);
1420
1441
  }
1421
1442
  }
1443
+ /** Detaches a part component from change detection and destroys it. */
1444
+ releasePartComponent(componentRef) {
1445
+ this.appRef.detachView(componentRef.hostView);
1446
+ componentRef.destroy();
1447
+ }
1422
1448
  updatePanelEditModes() {
1423
1449
  // Update IsEditing on all dynamic components
1424
1450
  this._panelComponents.forEach((entry) => {
@@ -1453,7 +1479,7 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1453
1479
  } if (rf & 2) {
1454
1480
  let _t;
1455
1481
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.LayoutContainer = _t.first);
1456
- } }, inputs: { Dashboard: "Dashboard", dashboard: "dashboard", DashboardId: "DashboardId", dashboardId: "dashboardId", IsEditing: "IsEditing", isEditing: "isEditing", ShowToolbar: "ShowToolbar", showToolbar: "showToolbar", AutoSave: "AutoSave", autoSave: "autoSave", ShowBreadcrumb: "ShowBreadcrumb", showBreadcrumb: "showBreadcrumb", ShowOpenInTabButton: "ShowOpenInTabButton", showOpenInTabButton: "showOpenInTabButton", ShowEditButton: "ShowEditButton", showEditButton: "showEditButton", Categories: "Categories" }, outputs: { configChanged: "configChanged", NavigationRequested: "NavigationRequested", navigationRequested: "navigationRequested", PanelInteraction: "PanelInteraction", panelInteraction: "panelInteraction", DashboardSaved: "DashboardSaved", dashboardSaved: "dashboardSaved", error: "error", EditModeChanged: "EditModeChanged", editModeChanged: "editModeChanged", BreadcrumbNavigate: "BreadcrumbNavigate", breadcrumbNavigate: "breadcrumbNavigate", OpenInTab: "OpenInTab", openInTab: "openInTab", LayoutLifecycle: "LayoutLifecycle", layoutLifecycle: "layoutLifecycle", LayoutReady: "LayoutReady", layoutReady: "layoutReady", LayoutDeferred: "LayoutDeferred", layoutDeferred: "layoutDeferred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 7, vars: 8, consts: [["layoutContainer", ""], [1, "dashboard-viewer"], ["Size", "large", "RootIcon", "fa-solid fa-gauge-high", "RootLabel", "Dashboards", 3, "Categories", "CurrentCategoryId", "CurrentDashboard", "ShowDashboardName", "AllowDragDrop"], [1, "dashboard-toolbar"], [1, "loading-overlay"], [1, "layout-container"], ["Icon", "fa-solid fa-layer-group", "Title", "No parts configured", "Message", "This dashboard has no parts yet. Add your first part to start visualizing your data.", "ActionIcon", "fa-solid fa-plus", 1, "dashboard-empty", 3, "ActionText"], ["Size", "large", "RootIcon", "fa-solid fa-gauge-high", "RootLabel", "Dashboards", 3, "Navigate", "Categories", "CurrentCategoryId", "CurrentDashboard", "ShowDashboardName", "AllowDragDrop"], [1, "toolbar-left"], [1, "dashboard-title"], [1, "toolbar-center"], [1, "unsaved-indicator"], [1, "toolbar-right"], ["title", "Open in its own tab", 1, "toolbar-button"], [1, "toolbar-button"], [1, "toolbar-button", 3, "active"], [1, "toolbar-button", "primary"], [1, "fa-solid", "fa-chart-line"], [1, "fa-solid", "fa-circle"], ["title", "Open in its own tab", 1, "toolbar-button", 3, "click"], [1, "fa-solid", "fa-up-right-from-square"], [1, "toolbar-button", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "fa-solid", "fa-edit"], [1, "toolbar-button", "primary", 3, "click"], [1, "fa-solid", "fa-save"], ["text", "Loading dashboard..."], ["Icon", "fa-solid fa-layer-group", "Title", "No parts configured", "Message", "This dashboard has no parts yet. Add your first part to start visualizing your data.", "ActionIcon", "fa-solid fa-plus", 1, "dashboard-empty", 3, "Action", "ActionText"]], template: function DashboardViewerComponent_Template(rf, ctx) { if (rf & 1) {
1482
+ } }, inputs: { Dashboard: "Dashboard", dashboard: "dashboard", DashboardId: "DashboardId", dashboardId: "dashboardId", IsEditing: "IsEditing", isEditing: "isEditing", CanEdit: "CanEdit", ShowToolbar: "ShowToolbar", showToolbar: "showToolbar", AutoSave: "AutoSave", autoSave: "autoSave", ShowBreadcrumb: "ShowBreadcrumb", showBreadcrumb: "showBreadcrumb", ShowOpenInTabButton: "ShowOpenInTabButton", showOpenInTabButton: "showOpenInTabButton", ShowEditButton: "ShowEditButton", showEditButton: "showEditButton", Categories: "Categories" }, outputs: { configChanged: "configChanged", NavigationRequested: "NavigationRequested", navigationRequested: "navigationRequested", PanelInteraction: "PanelInteraction", panelInteraction: "panelInteraction", DashboardSaved: "DashboardSaved", dashboardSaved: "dashboardSaved", error: "error", EditModeChanged: "EditModeChanged", editModeChanged: "editModeChanged", BreadcrumbNavigate: "BreadcrumbNavigate", breadcrumbNavigate: "breadcrumbNavigate", OpenInTab: "OpenInTab", openInTab: "openInTab", LayoutLifecycle: "LayoutLifecycle", layoutLifecycle: "layoutLifecycle", LayoutReady: "LayoutReady", layoutReady: "layoutReady", LayoutDeferred: "LayoutDeferred", layoutDeferred: "layoutDeferred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 7, vars: 8, consts: [["layoutContainer", ""], [1, "dashboard-viewer"], ["Size", "large", "RootIcon", "fa-solid fa-gauge-high", "RootLabel", "Dashboards", 3, "Categories", "CurrentCategoryId", "CurrentDashboard", "ShowDashboardName", "AllowDragDrop"], [1, "dashboard-toolbar"], [1, "loading-overlay"], [1, "layout-container"], ["Icon", "fa-solid fa-table-columns", "Title", "No parts yet", "Message", "Add a view, query, artifact, or web page to get started.", "ActionIcon", "fa-solid fa-plus", 1, "dashboard-empty", 3, "ActionText"], ["Size", "large", "RootIcon", "fa-solid fa-gauge-high", "RootLabel", "Dashboards", 3, "Navigate", "Categories", "CurrentCategoryId", "CurrentDashboard", "ShowDashboardName", "AllowDragDrop"], [1, "toolbar-left"], [1, "dashboard-title"], [1, "toolbar-center"], [1, "unsaved-indicator"], [1, "toolbar-right"], ["title", "Open in a dashboard tab", 1, "toolbar-button"], [1, "toolbar-button"], [1, "toolbar-button", 3, "active"], [1, "toolbar-button", "primary"], [1, "fa-solid", "fa-chart-line"], [1, "fa-solid", "fa-circle"], ["title", "Open in a dashboard tab", 1, "toolbar-button", 3, "click"], [1, "fa-solid", "fa-up-right-from-square"], [1, "toolbar-button", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "fa-solid", "fa-edit"], [1, "toolbar-button", "primary", 3, "click"], [1, "fa-solid", "fa-save"], ["text", "Loading dashboard..."], ["Icon", "fa-solid fa-table-columns", "Title", "No parts yet", "Message", "Add a view, query, artifact, or web page to get started.", "ActionIcon", "fa-solid fa-plus", 1, "dashboard-empty", 3, "Action", "ActionText"]], template: function DashboardViewerComponent_Template(rf, ctx) { if (rf & 1) {
1457
1483
  i0.ɵɵelementStart(0, "div", 1);
1458
1484
  i0.ɵɵconditionalCreate(1, DashboardViewerComponent_Conditional_1_Template, 1, 5, "mj-dashboard-breadcrumb", 2);
1459
1485
  i0.ɵɵconditionalCreate(2, DashboardViewerComponent_Conditional_2_Template, 10, 6, "div", 3);
@@ -1470,12 +1496,12 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1470
1496
  i0.ɵɵadvance();
1471
1497
  i0.ɵɵconditional(ctx.isLoading ? 3 : -1);
1472
1498
  i0.ɵɵadvance(3);
1473
- i0.ɵɵconditional(!ctx.hasPanels && !ctx.isLoading ? 6 : -1);
1474
- } }, dependencies: [i1.LoadingComponent, i2.MJEmptyStateComponent, i3.DashboardBreadcrumbComponent], styles: ["/**\n * Dashboard Viewer Component Styles\n *\n * IMPORTANT: For Golden Layout tabs to display correctly, you must import\n * Golden Layout's CSS in your application's global styles:\n *\n * @import 'golden-layout/dist/css/goldenlayout-base.css';\n *\n * Or add to your angular.json styles array:\n * \"node_modules/golden-layout/dist/css/goldenlayout-base.css\"\n */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dashboard-viewer {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--dashboard-bg, var(--mj-bg-surface-card));\n}\n\n.dashboard-viewer.editing .layout-container {\n outline: 2px dashed var(--dashboard-edit-outline, var(--mj-brand-primary));\n outline-offset: -2px;\n}\n\n/* ========================================\n Toolbar\n ======================================== */\n\n.dashboard-toolbar {\n flex: 0 0 48px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background: var(--dashboard-toolbar-bg, var(--mj-bg-surface));\n border-bottom: 1px solid var(--dashboard-border, var(--mj-border-default));\n box-sizing: border-box;\n z-index: 10;\n}\n\n.toolbar-left,\n.toolbar-center,\n.toolbar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.toolbar-left {\n flex: 1;\n}\n\n.toolbar-center {\n flex: 0 0 auto;\n}\n\n.toolbar-right {\n flex: 1;\n justify-content: flex-end;\n}\n\n.dashboard-title {\n font-size: 16px;\n font-weight: 500;\n color: var(--dashboard-title-color, var(--mj-text-primary));\n}\n\n.dashboard-title i {\n margin-right: 8px;\n color: var(--dashboard-title-icon, var(--mj-brand-primary));\n}\n\n.unsaved-indicator {\n font-size: 12px;\n color: var(--dashboard-unsaved-color, var(--mj-status-warning));\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.unsaved-indicator i {\n font-size: 8px;\n}\n\n.toolbar-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: 1px solid var(--dashboard-button-border, var(--mj-border-strong));\n border-radius: 4px;\n background: var(--dashboard-button-bg, var(--mj-bg-surface));\n color: var(--dashboard-button-color, var(--mj-text-primary));\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.toolbar-button:hover {\n background: var(--dashboard-button-hover-bg, var(--mj-bg-surface-card));\n border-color: var(--dashboard-button-hover-border, var(--mj-border-strong));\n}\n\n.toolbar-button.active {\n background: var(--dashboard-button-active-bg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)));\n border-color: var(--dashboard-button-active-border, var(--mj-brand-primary));\n color: var(--dashboard-button-active-color, var(--mj-brand-primary));\n}\n\n.toolbar-button.primary {\n background: var(--dashboard-button-primary-bg, var(--mj-brand-primary));\n border-color: var(--dashboard-button-primary-border, var(--mj-brand-primary-hover));\n color: var(--dashboard-button-primary-color, var(--mj-text-inverse));\n}\n\n.toolbar-button.primary:hover {\n background: var(--dashboard-button-primary-hover-bg, var(--mj-brand-primary-hover));\n}\n\n.toolbar-button i {\n font-size: 14px;\n}\n\n/* ========================================\n Layout Container\n ======================================== */\n\n/* Layout container fills remaining space in the flex column */\n.layout-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n min-height: 0;\n}\n\n/* ========================================\n Dashboard Panel Content (our wrapper)\n ======================================== */\n\n/* Dashboard panel wrapper (header + content) */\n:host ::ng-deep .dashboard-part-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n}\n\n/* Dashboard part header */\n:host ::ng-deep .dashboard-part-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n min-height: 40px;\n flex-shrink: 0;\n}\n\n/* Dashboard part content */\n:host ::ng-deep .dashboard-part-content {\n flex: 1;\n overflow: auto;\n min-height: 0;\n}\n\n/* ========================================\n Loading Overlay\n ======================================== */\n\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-surface) 90%, transparent);\n z-index: 100;\n}\n\n/* ========================================\n Empty State\n ======================================== */\n\n/* Empty state centered in the dashboard pane (mj-empty-state centers its own\n content; absolute positioning keeps it overlaid on the layout container). */\n.dashboard-empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 400px;\n}\n\n/* ========================================\n Panel Error State\n ======================================== */\n\n:host ::ng-deep .panel-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 24px;\n text-align: center;\n color: var(--dashboard-error-color, var(--mj-status-error));\n background: var(--dashboard-error-bg, color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface)));\n}\n\n:host ::ng-deep .panel-error i {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n:host ::ng-deep .panel-error span {\n font-size: 14px;\n}\n\n/* ========================================\n Golden Layout Overrides\n These require ViewEncapsulation.None to work on dynamic GL elements\n ======================================== */\n\n/* Override shell's hide-tab-bar rule that hides GL headers when dashboard is inside shell */\n/* The shell hides .lm_header when mj-tab-container has hide-tab-bar class */\n/* We need to ensure OUR Golden Layout headers are always visible */\nmj-dashboard-viewer .lm_header {\n display: block !important;\n}\n\n/* Ensure GL root container fills available space */\nmj-dashboard-viewer .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-dashboard-viewer .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-dashboard-viewer .lm_item,\nmj-dashboard-viewer .lm_content,\nmj-dashboard-viewer .lm_stack,\nmj-dashboard-viewer .lm_row,\nmj-dashboard-viewer .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-dashboard-viewer .lm_item {\n overflow: hidden !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks */\nmj-dashboard-viewer .lm_items {\n width: 100% !important;\n height: calc(100% - 38px) !important; /* Account for header height (38px) */\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-dashboard-viewer .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the ComponentItem div inside lm_items */\nmj-dashboard-viewer .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-dashboard-viewer .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout */\nmj-dashboard-viewer .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds */\nmj-dashboard-viewer .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_content {\n background: var(--mj-bg-surface) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_item_container {\n background: var(--mj-bg-surface) !important;\n}\n\n/* Tab header styling */\nmj-dashboard-viewer .lm_header {\n height: 38px !important;\n padding-top: 2px !important;\n padding-left: 4px !important;\n background: var(--mj-bg-surface-card) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\nmj-dashboard-viewer .lm_tabs {\n height: 36px !important;\n}\n\n/* Hide Golden Layout window controls (popout, maximize) */\nmj-dashboard-viewer .lm_controls {\n display: none !important;\n}\n\n/* Tab styling */\nmj-dashboard-viewer .lm_header .lm_tab {\n padding: 0 16px 0 28px !important; /* Extra left padding for icon */\n font-size: 13px !important;\n height: 35px !important;\n line-height: 35px !important;\n box-sizing: border-box !important;\n cursor: pointer !important;\n user-select: none !important;\n background: transparent !important;\n border: none !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n transition: all 0.15s ease !important;\n position: relative !important;\n z-index: 1 !important;\n margin-right: 1px !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab:hover {\n background: var(--mj-bg-surface-sunken) !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab.lm_active {\n background: var(--mj-bg-surface) !important;\n height: 36px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid var(--mj-border-default) !important;\n border-bottom-color: var(--mj-bg-surface) !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\nmj-dashboard-viewer .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n}\n\n/* Panel icon styling - icon is direct child of .lm_tab to preserve GL's drag/drop */\n/* Positioned absolutely in the left padding area so it doesn't affect GL's title structure */\nmj-dashboard-viewer .lm_header .lm_tab .panel-icon {\n position: absolute !important;\n left: 10px !important;\n top: 50% !important;\n transform: translateY(-30%) !important;\n font-family: \"Font Awesome 6 Free\", \"Font Awesome 6 Pro\", \"Font Awesome 5 Free\", \"Font Awesome 5 Pro\", FontAwesome !important;\n font-weight: 900 !important; /* Required for solid icons */\n font-size: 12px !important;\n color: var(--mj-brand-primary) !important;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nmj-dashboard-viewer .lm_close_tab {\n position: absolute !important;\n right: 4px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 16px !important;\n height: 16px !important;\n cursor: pointer !important;\n opacity: 0 !important;\n transition: all 0.15s ease !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\nmj-dashboard-viewer .lm_close_tab:hover {\n opacity: 1 !important;\n color: var(--mj-status-error) !important;\n}\n\n/* Add padding for close button when editing */\nmj-dashboard-viewer .dashboard-viewer.editing .lm_header .lm_tab {\n padding-right: 24px !important;\n}\n\n/* ========================================\n View Mode (Not Editing) - Lock Layout\n ======================================== */\n\n/* Hide close buttons on tabs when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_close_tab {\n display: none !important;\n}\n\n/* Remove extra padding for close button when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n padding-right: 16px !important;\n}\n\n/* Disable splitter dragging when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter {\n pointer-events: none !important;\n cursor: default !important;\n}\n\n/* Hide splitter drag handle visual when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter .lm_drag_handle {\n display: none !important;\n}\n\n/* Disable tab dragging cursor when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n cursor: default !important;\n}\n\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_title {\n cursor: default !important;\n}\n"], encapsulation: 2 });
1499
+ i0.ɵɵconditional(!ctx.HasPanels && !ctx.isLoading ? 6 : -1);
1500
+ } }, dependencies: [i1.LoadingComponent, i2.MJEmptyStateComponent, i3.DashboardBreadcrumbComponent], styles: ["/**\n * Dashboard Viewer Component Styles\n *\n * IMPORTANT: For Golden Layout tabs to display correctly, you must import\n * Golden Layout's CSS in your application's global styles:\n *\n * @import 'golden-layout/dist/css/goldenlayout-base.css';\n *\n * Or add to your angular.json styles array:\n * \"node_modules/golden-layout/dist/css/goldenlayout-base.css\"\n */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dashboard-viewer {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--dashboard-bg, var(--mj-bg-surface-card));\n}\n\n/* ========================================\n Toolbar\n ======================================== */\n\n.dashboard-toolbar {\n flex: 0 0 48px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background: var(--dashboard-toolbar-bg, var(--mj-bg-surface));\n border-bottom: 1px solid var(--dashboard-border, var(--mj-border-default));\n box-sizing: border-box;\n z-index: 10;\n}\n\n.toolbar-left,\n.toolbar-center,\n.toolbar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.toolbar-left {\n flex: 1;\n}\n\n.toolbar-center {\n flex: 0 0 auto;\n}\n\n.toolbar-right {\n flex: 1;\n justify-content: flex-end;\n}\n\n.dashboard-title {\n font-size: 16px;\n font-weight: 500;\n color: var(--dashboard-title-color, var(--mj-text-primary));\n}\n\n.dashboard-title i {\n margin-right: 8px;\n color: var(--dashboard-title-icon, var(--mj-brand-primary));\n}\n\n.unsaved-indicator {\n font-size: 12px;\n color: var(--dashboard-unsaved-color, var(--mj-status-warning));\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.unsaved-indicator i {\n font-size: 8px;\n}\n\n.toolbar-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: 1px solid var(--dashboard-button-border, var(--mj-border-strong));\n border-radius: 4px;\n background: var(--dashboard-button-bg, var(--mj-bg-surface));\n color: var(--dashboard-button-color, var(--mj-text-primary));\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.toolbar-button:hover {\n background: var(--dashboard-button-hover-bg, var(--mj-bg-surface-card));\n border-color: var(--dashboard-button-hover-border, var(--mj-border-strong));\n}\n\n.toolbar-button.active {\n background: var(--dashboard-button-active-bg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)));\n border-color: var(--dashboard-button-active-border, var(--mj-brand-primary));\n color: var(--dashboard-button-active-color, var(--mj-brand-primary));\n}\n\n.toolbar-button.primary {\n background: var(--dashboard-button-primary-bg, var(--mj-brand-primary));\n border-color: var(--dashboard-button-primary-border, var(--mj-brand-primary-hover));\n color: var(--dashboard-button-primary-color, var(--mj-text-inverse));\n}\n\n.toolbar-button.primary:hover {\n background: var(--dashboard-button-primary-hover-bg, var(--mj-brand-primary-hover));\n}\n\n.toolbar-button i {\n font-size: 14px;\n}\n\n/* ========================================\n Layout Container\n ======================================== */\n\n/* Layout container fills remaining space in the flex column */\n.layout-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n min-height: 0;\n}\n\n/* ========================================\n Dashboard Panel Content (our wrapper)\n ======================================== */\n\n/* Dashboard panel wrapper (holds the part content) */\n:host ::ng-deep .dashboard-part-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n}\n\n/* Dashboard part content */\n:host ::ng-deep .dashboard-part-content {\n flex: 1;\n overflow: auto;\n min-height: 0;\n}\n\n/* ========================================\n Loading Overlay\n ======================================== */\n\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-surface) 90%, transparent);\n z-index: 100;\n}\n\n/* ========================================\n Empty State\n ======================================== */\n\n/* Empty state centered in the dashboard pane (mj-empty-state centers its own\n content; absolute positioning keeps it overlaid on the layout container). */\n.dashboard-empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 400px;\n}\n\n/* ========================================\n Panel Error State\n ======================================== */\n\n:host ::ng-deep .panel-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 24px;\n text-align: center;\n color: var(--dashboard-error-color, var(--mj-status-error));\n background: var(--dashboard-error-bg, color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface)));\n}\n\n:host ::ng-deep .panel-error i {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n:host ::ng-deep .panel-error span {\n font-size: 14px;\n}\n\n/* ========================================\n Golden Layout Overrides\n These require ViewEncapsulation.None to work on dynamic GL elements\n ======================================== */\n\n/* Override shell's hide-tab-bar rule that hides GL headers when dashboard is inside shell */\n/* The shell hides .lm_header when mj-tab-container has hide-tab-bar class */\n/* We need to ensure OUR Golden Layout headers are always visible */\nmj-dashboard-viewer .lm_header {\n display: block !important;\n}\n\n/* Ensure GL root container fills available space */\nmj-dashboard-viewer .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-dashboard-viewer .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-dashboard-viewer .lm_item,\nmj-dashboard-viewer .lm_content,\nmj-dashboard-viewer .lm_stack,\nmj-dashboard-viewer .lm_row,\nmj-dashboard-viewer .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-dashboard-viewer .lm_item {\n overflow: hidden !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks (its height is set with the stack header below) */\nmj-dashboard-viewer .lm_items {\n width: 100% !important;\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-dashboard-viewer .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the ComponentItem div inside lm_items */\nmj-dashboard-viewer .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-dashboard-viewer .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout */\nmj-dashboard-viewer .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds */\nmj-dashboard-viewer .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_content {\n background: var(--mj-bg-surface) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_item_container {\n background: var(--mj-bg-surface) !important;\n}\n\n/* ===== Golden Layout: part tabs and part buttons =====\n The shell's tab-container and theme rules reach this nested layout. These selectors add\n .dashboard-viewer to outrank them, with !important where those rules use it. */\n\n/* Stack header: tab track on the left, part buttons on the right (DASHBOARD_STACK_HEADER_HEIGHT) */\nmj-dashboard-viewer .dashboard-viewer .lm_header {\n display: block !important;\n height: 44px !important;\n max-height: none !important; /* shell.component.css caps nested headers at 40px */\n padding: 0 !important;\n background: var(--mj-bg-surface) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n z-index: auto; /* no stacking context, so the tab dropdown list can rise above the part */\n}\nmj-dashboard-viewer .dashboard-viewer .lm_items {\n height: calc(100% - 44px) !important;\n}\n\n/* The tab track, styled like mj-tab-nav. Golden Layout counts each tab's width and right margin; the rest of the\n spacing beside the tabs is DASHBOARD_TAB_CONTROL_OFFSET (78px): the track's 12px inset, 1px borders and 4px left\n padding, an 8px gap, the 34px tab dropdown button and its 6px gap, and the controls' 12px inset. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabs {\n top: 5px !important;\n left: var(--mj-space-3) !important;\n right: auto !important; /* shell sets left:0; right:0; overflow-x:auto */\n height: auto !important;\n overflow: visible !important;\n padding: var(--mj-space-1) 0 var(--mj-space-1) var(--mj-space-1); /* the last tab's margin closes the right side */\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab {\n display: inline-flex !important;\n align-items: center !important;\n gap: var(--mj-space-2) !important;\n flex: 0 0 auto !important;\n height: auto !important;\n margin: 0 var(--mj-space-1) 0 0 !important;\n padding: 4px var(--mj-space-3) !important;\n font-family: inherit !important;\n font-size: 13px !important;\n font-weight: 500 !important;\n line-height: 1.2 !important;\n color: var(--mj-text-secondary) !important;\n background: transparent !important;\n border: none !important;\n border-radius: var(--mj-radius-md) !important;\n box-shadow: none !important;\n cursor: pointer !important;\n transition: background-color var(--mj-transition-fast), color var(--mj-transition-fast) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab::after { content: none !important; } /* shell pipe separators */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab:hover:not(.lm_active) {\n background: color-mix(in srgb, var(--mj-bg-surface) 70%, transparent) !important;\n color: var(--mj-text-primary) !important;\n}\n/* The shell's active tab rule is as specific as the tab rule above, so the tab's box is repeated here */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab.lm_active {\n height: auto !important;\n margin: 0 var(--mj-space-1) 0 0 !important;\n border: none !important;\n border-radius: var(--mj-radius-md) !important;\n background: var(--mj-bg-surface) !important;\n color: var(--mj-brand-primary) !important;\n box-shadow: var(--mj-shadow-sm) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n}\nmj-dashboard-viewer .dashboard-viewer.editing .lm_header .lm_tab { cursor: grab !important; }\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_title {\n cursor: inherit !important;\n line-height: normal !important;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab .panel-icon { /* the part's icon */\n position: static !important;\n width: auto !important;\n height: auto !important;\n transform: none !important;\n font-size: 13px !important;\n color: inherit !important;\n}\n\n/* Parts are removed only through Remove (with a confirmation): hide GL's close buttons */\nmj-dashboard-viewer .dashboard-viewer .lm_close_tab,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_close,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_popout,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_maximise { display: none !important; }\n\n/* Part buttons (Edit part, Remove) in the header's controls area */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls {\n top: 0 !important;\n bottom: 0 !important;\n right: var(--mj-space-3) !important;\n z-index: 3; /* above tabs that run into it (the shell gives tabs 1 and 2), under the tab dropdown list */\n align-items: center;\n gap: 6px;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .dashboard-stack-actions {\n display: flex !important;\n align-items: center;\n gap: 6px;\n width: auto !important; /* GL base: .lm_controls > * is 18x18 and floats */\n height: auto !important;\n float: none !important;\n opacity: 1 !important; /* MJ theme: .lm_controls > * is at 0.6 */\n cursor: default !important;\n}\n\n/* Golden Layout's tab dropdown button, sized like the part buttons and placed left of them. It sits outside the\n controls' measured width, because Golden Layout shows it only after it measures the tabs. Golden Layout shows\n and hides it with an inline display, so display here carries no !important. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown {\n display: inline-flex;\n position: absolute;\n top: 0;\n bottom: 0;\n right: 100%;\n margin: auto 6px auto 0;\n width: 32px !important; /* GL base: .lm_controls > * is 18x18 and floats */\n height: 32px !important;\n float: none !important;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: var(--mj-radius-md);\n color: var(--mj-text-secondary);\n opacity: 1 !important; /* MJ theme: .lm_controls > * is at 0.6 */\n cursor: pointer;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown:hover {\n background-color: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown::before {\n content: '\\f078'; /* chevron-down, in place of GL's border triangle */\n width: auto;\n height: auto;\n border: none;\n font-family: 'Font Awesome 6 Free';\n font-weight: 900;\n font-size: 12px;\n line-height: 1;\n color: inherit;\n}\n\n/* Tabs that do not fit: Golden Layout's tab dropdown list, as a menu below the header. Golden Layout shows and\n hides it with an inline display, so display here carries no !important. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list {\n display: flex;\n flex-direction: column;\n top: calc(100% + var(--mj-space-1));\n right: var(--mj-space-3);\n z-index: var(--mj-z-dropdown);\n min-width: 200px;\n max-height: 290px;\n overflow-y: auto;\n padding: var(--mj-space-1);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-md);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_tab {\n display: flex !important;\n margin: 0 !important;\n padding: 6px var(--mj-space-2) !important;\n border-radius: var(--mj-radius-sm) !important;\n background: transparent !important;\n box-shadow: none !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_tab:hover:not(.lm_active) {\n background: var(--mj-bg-surface-hover) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_title {\n width: auto; /* GL base: 100px */\n}\n\n/* Each stack (a panel of part tabs) is a card */\nmj-dashboard-viewer .dashboard-viewer .lm_stack {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default) !important;\n border-radius: var(--mj-radius-lg);\n}\n\n/* ========================================\n View Mode (Not Editing) - Lock Layout\n ======================================== */\n\n/* Disable splitter dragging when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter {\n pointer-events: none !important;\n cursor: default !important;\n}\n\n/* Hide splitter drag handle visual when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter .lm_drag_handle {\n display: none !important;\n}\n\n/* Disable tab dragging cursor when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n cursor: default !important;\n}\n\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_title {\n cursor: default !important;\n}\n"], encapsulation: 2 });
1475
1501
  }
1476
1502
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(DashboardViewerComponent, [{
1477
1503
  type: Component,
1478
- args: [{ standalone: false, selector: 'mj-dashboard-viewer', encapsulation: ViewEncapsulation.None, template: "<!-- Dashboard Viewer Component -->\n<div class=\"dashboard-viewer\" [class.editing]=\"isEditing\" [class.has-toolbar]=\"shouldShowToolbar\">\n <!-- Breadcrumb Navigation (hidden in edit mode) -->\n @if (showBreadcrumb && !isEditing && dashboard) {\n <mj-dashboard-breadcrumb\n [Categories]=\"Categories\"\n [CurrentCategoryId]=\"dashboard.CategoryID\"\n [CurrentDashboard]=\"dashboard\"\n [ShowDashboardName]=\"true\"\n [AllowDragDrop]=\"false\"\n Size=\"large\"\n RootIcon=\"fa-solid fa-gauge-high\"\n RootLabel=\"Dashboards\"\n (Navigate)=\"onBreadcrumbNavigate($event)\">\n </mj-dashboard-breadcrumb>\n }\n\n <!-- Toolbar (auto-hides when all elements are disabled) -->\n @if (shouldShowToolbar) {\n <div class=\"dashboard-toolbar\">\n <div class=\"toolbar-left\">\n @if (dashboard && (isEditing || !showBreadcrumb)) {\n <span class=\"dashboard-title\">\n <i class=\"fa-solid fa-chart-line\"></i>\n {{ dashboard.Name }}\n </span>\n }\n </div>\n <div class=\"toolbar-center\">\n @if (hasUnsavedChanges && isEditing) {\n <span class=\"unsaved-indicator\">\n <i class=\"fa-solid fa-circle\"></i>\n Unsaved changes\n </span>\n }\n </div>\n <div class=\"toolbar-right\">\n <!-- Open in New Tab button (when embedded) -->\n @if (showOpenInTabButton && !isEditing) {\n <button\n class=\"toolbar-button\"\n (click)=\"onOpenInTabClick()\"\n title=\"Open in its own tab\">\n <i class=\"fa-solid fa-up-right-from-square\"></i>\n Open in Tab\n </button>\n }\n @if (isEditing) {\n <button\n class=\"toolbar-button\"\n (click)=\"onAddPanelClick()\">\n <i class=\"fa-solid fa-plus\"></i>\n Add Part\n </button>\n }\n @if (showEditButton) {\n <button\n class=\"toolbar-button\"\n [class.active]=\"isEditing\"\n (click)=\"toggleEditMode()\">\n <i class=\"fa-solid fa-edit\"></i>\n {{ isEditing ? 'Editing' : 'Edit' }}\n </button>\n }\n @if (isEditing && hasUnsavedChanges) {\n <button\n class=\"toolbar-button primary\"\n (click)=\"save()\">\n <i class=\"fa-solid fa-save\"></i>\n Save\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Loading Overlay -->\n @if (isLoading) {\n <div class=\"loading-overlay\">\n <mj-loading text=\"Loading dashboard...\"></mj-loading>\n </div>\n }\n\n <!-- Layout Container -->\n <div class=\"layout-container\" #layoutContainer>\n <!-- Golden Layout will render panels here -->\n </div>\n\n <!-- Empty State -->\n @if (!hasPanels && !isLoading) {\n <mj-empty-state\n class=\"dashboard-empty\"\n Icon=\"fa-solid fa-layer-group\"\n Title=\"No parts configured\"\n Message=\"This dashboard has no parts yet. Add your first part to start visualizing your data.\"\n [ActionText]=\"isEditing ? 'Add Your First Part' : ''\"\n ActionIcon=\"fa-solid fa-plus\"\n (Action)=\"onAddPanelClick()\" />\n }\n</div>\n", styles: ["/**\n * Dashboard Viewer Component Styles\n *\n * IMPORTANT: For Golden Layout tabs to display correctly, you must import\n * Golden Layout's CSS in your application's global styles:\n *\n * @import 'golden-layout/dist/css/goldenlayout-base.css';\n *\n * Or add to your angular.json styles array:\n * \"node_modules/golden-layout/dist/css/goldenlayout-base.css\"\n */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dashboard-viewer {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--dashboard-bg, var(--mj-bg-surface-card));\n}\n\n.dashboard-viewer.editing .layout-container {\n outline: 2px dashed var(--dashboard-edit-outline, var(--mj-brand-primary));\n outline-offset: -2px;\n}\n\n/* ========================================\n Toolbar\n ======================================== */\n\n.dashboard-toolbar {\n flex: 0 0 48px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background: var(--dashboard-toolbar-bg, var(--mj-bg-surface));\n border-bottom: 1px solid var(--dashboard-border, var(--mj-border-default));\n box-sizing: border-box;\n z-index: 10;\n}\n\n.toolbar-left,\n.toolbar-center,\n.toolbar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.toolbar-left {\n flex: 1;\n}\n\n.toolbar-center {\n flex: 0 0 auto;\n}\n\n.toolbar-right {\n flex: 1;\n justify-content: flex-end;\n}\n\n.dashboard-title {\n font-size: 16px;\n font-weight: 500;\n color: var(--dashboard-title-color, var(--mj-text-primary));\n}\n\n.dashboard-title i {\n margin-right: 8px;\n color: var(--dashboard-title-icon, var(--mj-brand-primary));\n}\n\n.unsaved-indicator {\n font-size: 12px;\n color: var(--dashboard-unsaved-color, var(--mj-status-warning));\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.unsaved-indicator i {\n font-size: 8px;\n}\n\n.toolbar-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: 1px solid var(--dashboard-button-border, var(--mj-border-strong));\n border-radius: 4px;\n background: var(--dashboard-button-bg, var(--mj-bg-surface));\n color: var(--dashboard-button-color, var(--mj-text-primary));\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.toolbar-button:hover {\n background: var(--dashboard-button-hover-bg, var(--mj-bg-surface-card));\n border-color: var(--dashboard-button-hover-border, var(--mj-border-strong));\n}\n\n.toolbar-button.active {\n background: var(--dashboard-button-active-bg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)));\n border-color: var(--dashboard-button-active-border, var(--mj-brand-primary));\n color: var(--dashboard-button-active-color, var(--mj-brand-primary));\n}\n\n.toolbar-button.primary {\n background: var(--dashboard-button-primary-bg, var(--mj-brand-primary));\n border-color: var(--dashboard-button-primary-border, var(--mj-brand-primary-hover));\n color: var(--dashboard-button-primary-color, var(--mj-text-inverse));\n}\n\n.toolbar-button.primary:hover {\n background: var(--dashboard-button-primary-hover-bg, var(--mj-brand-primary-hover));\n}\n\n.toolbar-button i {\n font-size: 14px;\n}\n\n/* ========================================\n Layout Container\n ======================================== */\n\n/* Layout container fills remaining space in the flex column */\n.layout-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n min-height: 0;\n}\n\n/* ========================================\n Dashboard Panel Content (our wrapper)\n ======================================== */\n\n/* Dashboard panel wrapper (header + content) */\n:host ::ng-deep .dashboard-part-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n}\n\n/* Dashboard part header */\n:host ::ng-deep .dashboard-part-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n min-height: 40px;\n flex-shrink: 0;\n}\n\n/* Dashboard part content */\n:host ::ng-deep .dashboard-part-content {\n flex: 1;\n overflow: auto;\n min-height: 0;\n}\n\n/* ========================================\n Loading Overlay\n ======================================== */\n\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-surface) 90%, transparent);\n z-index: 100;\n}\n\n/* ========================================\n Empty State\n ======================================== */\n\n/* Empty state centered in the dashboard pane (mj-empty-state centers its own\n content; absolute positioning keeps it overlaid on the layout container). */\n.dashboard-empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 400px;\n}\n\n/* ========================================\n Panel Error State\n ======================================== */\n\n:host ::ng-deep .panel-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 24px;\n text-align: center;\n color: var(--dashboard-error-color, var(--mj-status-error));\n background: var(--dashboard-error-bg, color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface)));\n}\n\n:host ::ng-deep .panel-error i {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n:host ::ng-deep .panel-error span {\n font-size: 14px;\n}\n\n/* ========================================\n Golden Layout Overrides\n These require ViewEncapsulation.None to work on dynamic GL elements\n ======================================== */\n\n/* Override shell's hide-tab-bar rule that hides GL headers when dashboard is inside shell */\n/* The shell hides .lm_header when mj-tab-container has hide-tab-bar class */\n/* We need to ensure OUR Golden Layout headers are always visible */\nmj-dashboard-viewer .lm_header {\n display: block !important;\n}\n\n/* Ensure GL root container fills available space */\nmj-dashboard-viewer .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-dashboard-viewer .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-dashboard-viewer .lm_item,\nmj-dashboard-viewer .lm_content,\nmj-dashboard-viewer .lm_stack,\nmj-dashboard-viewer .lm_row,\nmj-dashboard-viewer .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-dashboard-viewer .lm_item {\n overflow: hidden !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks */\nmj-dashboard-viewer .lm_items {\n width: 100% !important;\n height: calc(100% - 38px) !important; /* Account for header height (38px) */\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-dashboard-viewer .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the ComponentItem div inside lm_items */\nmj-dashboard-viewer .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-dashboard-viewer .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout */\nmj-dashboard-viewer .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds */\nmj-dashboard-viewer .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_content {\n background: var(--mj-bg-surface) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_item_container {\n background: var(--mj-bg-surface) !important;\n}\n\n/* Tab header styling */\nmj-dashboard-viewer .lm_header {\n height: 38px !important;\n padding-top: 2px !important;\n padding-left: 4px !important;\n background: var(--mj-bg-surface-card) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\nmj-dashboard-viewer .lm_tabs {\n height: 36px !important;\n}\n\n/* Hide Golden Layout window controls (popout, maximize) */\nmj-dashboard-viewer .lm_controls {\n display: none !important;\n}\n\n/* Tab styling */\nmj-dashboard-viewer .lm_header .lm_tab {\n padding: 0 16px 0 28px !important; /* Extra left padding for icon */\n font-size: 13px !important;\n height: 35px !important;\n line-height: 35px !important;\n box-sizing: border-box !important;\n cursor: pointer !important;\n user-select: none !important;\n background: transparent !important;\n border: none !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n transition: all 0.15s ease !important;\n position: relative !important;\n z-index: 1 !important;\n margin-right: 1px !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab:hover {\n background: var(--mj-bg-surface-sunken) !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab.lm_active {\n background: var(--mj-bg-surface) !important;\n height: 36px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid var(--mj-border-default) !important;\n border-bottom-color: var(--mj-bg-surface) !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\nmj-dashboard-viewer .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n}\n\n/* Panel icon styling - icon is direct child of .lm_tab to preserve GL's drag/drop */\n/* Positioned absolutely in the left padding area so it doesn't affect GL's title structure */\nmj-dashboard-viewer .lm_header .lm_tab .panel-icon {\n position: absolute !important;\n left: 10px !important;\n top: 50% !important;\n transform: translateY(-30%) !important;\n font-family: \"Font Awesome 6 Free\", \"Font Awesome 6 Pro\", \"Font Awesome 5 Free\", \"Font Awesome 5 Pro\", FontAwesome !important;\n font-weight: 900 !important; /* Required for solid icons */\n font-size: 12px !important;\n color: var(--mj-brand-primary) !important;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nmj-dashboard-viewer .lm_close_tab {\n position: absolute !important;\n right: 4px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 16px !important;\n height: 16px !important;\n cursor: pointer !important;\n opacity: 0 !important;\n transition: all 0.15s ease !important;\n}\n\nmj-dashboard-viewer .lm_header .lm_tab:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\nmj-dashboard-viewer .lm_close_tab:hover {\n opacity: 1 !important;\n color: var(--mj-status-error) !important;\n}\n\n/* Add padding for close button when editing */\nmj-dashboard-viewer .dashboard-viewer.editing .lm_header .lm_tab {\n padding-right: 24px !important;\n}\n\n/* ========================================\n View Mode (Not Editing) - Lock Layout\n ======================================== */\n\n/* Hide close buttons on tabs when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_close_tab {\n display: none !important;\n}\n\n/* Remove extra padding for close button when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n padding-right: 16px !important;\n}\n\n/* Disable splitter dragging when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter {\n pointer-events: none !important;\n cursor: default !important;\n}\n\n/* Hide splitter drag handle visual when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter .lm_drag_handle {\n display: none !important;\n}\n\n/* Disable tab dragging cursor when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n cursor: default !important;\n}\n\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_title {\n cursor: default !important;\n}\n"] }]
1504
+ args: [{ standalone: false, selector: 'mj-dashboard-viewer', encapsulation: ViewEncapsulation.None, template: "<!-- Dashboard Viewer Component -->\n<div class=\"dashboard-viewer\" [class.editing]=\"isEditing\" [class.has-toolbar]=\"shouldShowToolbar\">\n <!-- Breadcrumb Navigation (hidden in edit mode) -->\n @if (showBreadcrumb && !isEditing && dashboard) {\n <mj-dashboard-breadcrumb\n [Categories]=\"Categories\"\n [CurrentCategoryId]=\"dashboard.CategoryID\"\n [CurrentDashboard]=\"dashboard\"\n [ShowDashboardName]=\"true\"\n [AllowDragDrop]=\"false\"\n Size=\"large\"\n RootIcon=\"fa-solid fa-gauge-high\"\n RootLabel=\"Dashboards\"\n (Navigate)=\"onBreadcrumbNavigate($event)\">\n </mj-dashboard-breadcrumb>\n }\n\n <!-- Toolbar (auto-hides when all elements are disabled) -->\n @if (shouldShowToolbar) {\n <div class=\"dashboard-toolbar\">\n <div class=\"toolbar-left\">\n @if (dashboard && (isEditing || !showBreadcrumb)) {\n <span class=\"dashboard-title\">\n <i class=\"fa-solid fa-chart-line\"></i>\n {{ dashboard.Name }}\n </span>\n }\n </div>\n <div class=\"toolbar-center\">\n @if (hasUnsavedChanges && isEditing) {\n <span class=\"unsaved-indicator\">\n <i class=\"fa-solid fa-circle\"></i>\n Unsaved changes\n </span>\n }\n </div>\n <div class=\"toolbar-right\">\n <!-- Open in New Tab button (when embedded) -->\n @if (showOpenInTabButton && !isEditing) {\n <button\n class=\"toolbar-button\"\n (click)=\"onOpenInTabClick()\"\n title=\"Open in a dashboard tab\">\n <i class=\"fa-solid fa-up-right-from-square\"></i>\n Open in Tab\n </button>\n }\n @if (isEditing) {\n <button\n class=\"toolbar-button\"\n (click)=\"onAddPanelClick()\">\n <i class=\"fa-solid fa-plus\"></i>\n Add Part\n </button>\n }\n @if (showEditButton) {\n <button\n class=\"toolbar-button\"\n [class.active]=\"isEditing\"\n (click)=\"toggleEditMode()\">\n <i class=\"fa-solid fa-edit\"></i>\n {{ isEditing ? 'Editing' : 'Edit' }}\n </button>\n }\n @if (isEditing && hasUnsavedChanges) {\n <button\n class=\"toolbar-button primary\"\n (click)=\"save()\">\n <i class=\"fa-solid fa-save\"></i>\n Save\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Loading Overlay -->\n @if (isLoading) {\n <div class=\"loading-overlay\">\n <mj-loading text=\"Loading dashboard...\"></mj-loading>\n </div>\n }\n\n <!-- Layout Container -->\n <div class=\"layout-container\" #layoutContainer>\n <!-- Golden Layout will render panels here -->\n </div>\n\n <!-- Empty State: Add part shows in edit mode, and in view mode to a user who can edit -->\n @if (!HasPanels && !isLoading) {\n <mj-empty-state\n class=\"dashboard-empty\"\n Icon=\"fa-solid fa-table-columns\"\n Title=\"No parts yet\"\n Message=\"Add a view, query, artifact, or web page to get started.\"\n [ActionText]=\"IsEditing || CanEdit ? 'Add part' : ''\"\n ActionIcon=\"fa-solid fa-plus\"\n (Action)=\"OnAddPanelClick()\" />\n }\n</div>\n", styles: ["/**\n * Dashboard Viewer Component Styles\n *\n * IMPORTANT: For Golden Layout tabs to display correctly, you must import\n * Golden Layout's CSS in your application's global styles:\n *\n * @import 'golden-layout/dist/css/goldenlayout-base.css';\n *\n * Or add to your angular.json styles array:\n * \"node_modules/golden-layout/dist/css/goldenlayout-base.css\"\n */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.dashboard-viewer {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: var(--dashboard-bg, var(--mj-bg-surface-card));\n}\n\n/* ========================================\n Toolbar\n ======================================== */\n\n.dashboard-toolbar {\n flex: 0 0 48px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background: var(--dashboard-toolbar-bg, var(--mj-bg-surface));\n border-bottom: 1px solid var(--dashboard-border, var(--mj-border-default));\n box-sizing: border-box;\n z-index: 10;\n}\n\n.toolbar-left,\n.toolbar-center,\n.toolbar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.toolbar-left {\n flex: 1;\n}\n\n.toolbar-center {\n flex: 0 0 auto;\n}\n\n.toolbar-right {\n flex: 1;\n justify-content: flex-end;\n}\n\n.dashboard-title {\n font-size: 16px;\n font-weight: 500;\n color: var(--dashboard-title-color, var(--mj-text-primary));\n}\n\n.dashboard-title i {\n margin-right: 8px;\n color: var(--dashboard-title-icon, var(--mj-brand-primary));\n}\n\n.unsaved-indicator {\n font-size: 12px;\n color: var(--dashboard-unsaved-color, var(--mj-status-warning));\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.unsaved-indicator i {\n font-size: 8px;\n}\n\n.toolbar-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: 1px solid var(--dashboard-button-border, var(--mj-border-strong));\n border-radius: 4px;\n background: var(--dashboard-button-bg, var(--mj-bg-surface));\n color: var(--dashboard-button-color, var(--mj-text-primary));\n font-size: 13px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.toolbar-button:hover {\n background: var(--dashboard-button-hover-bg, var(--mj-bg-surface-card));\n border-color: var(--dashboard-button-hover-border, var(--mj-border-strong));\n}\n\n.toolbar-button.active {\n background: var(--dashboard-button-active-bg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)));\n border-color: var(--dashboard-button-active-border, var(--mj-brand-primary));\n color: var(--dashboard-button-active-color, var(--mj-brand-primary));\n}\n\n.toolbar-button.primary {\n background: var(--dashboard-button-primary-bg, var(--mj-brand-primary));\n border-color: var(--dashboard-button-primary-border, var(--mj-brand-primary-hover));\n color: var(--dashboard-button-primary-color, var(--mj-text-inverse));\n}\n\n.toolbar-button.primary:hover {\n background: var(--dashboard-button-primary-hover-bg, var(--mj-brand-primary-hover));\n}\n\n.toolbar-button i {\n font-size: 14px;\n}\n\n/* ========================================\n Layout Container\n ======================================== */\n\n/* Layout container fills remaining space in the flex column */\n.layout-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n min-height: 0;\n}\n\n/* ========================================\n Dashboard Panel Content (our wrapper)\n ======================================== */\n\n/* Dashboard panel wrapper (holds the part content) */\n:host ::ng-deep .dashboard-part-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface);\n}\n\n/* Dashboard part content */\n:host ::ng-deep .dashboard-part-content {\n flex: 1;\n overflow: auto;\n min-height: 0;\n}\n\n/* ========================================\n Loading Overlay\n ======================================== */\n\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-surface) 90%, transparent);\n z-index: 100;\n}\n\n/* ========================================\n Empty State\n ======================================== */\n\n/* Empty state centered in the dashboard pane (mj-empty-state centers its own\n content; absolute positioning keeps it overlaid on the layout container). */\n.dashboard-empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 400px;\n}\n\n/* ========================================\n Panel Error State\n ======================================== */\n\n:host ::ng-deep .panel-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 24px;\n text-align: center;\n color: var(--dashboard-error-color, var(--mj-status-error));\n background: var(--dashboard-error-bg, color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface)));\n}\n\n:host ::ng-deep .panel-error i {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n:host ::ng-deep .panel-error span {\n font-size: 14px;\n}\n\n/* ========================================\n Golden Layout Overrides\n These require ViewEncapsulation.None to work on dynamic GL elements\n ======================================== */\n\n/* Override shell's hide-tab-bar rule that hides GL headers when dashboard is inside shell */\n/* The shell hides .lm_header when mj-tab-container has hide-tab-bar class */\n/* We need to ensure OUR Golden Layout headers are always visible */\nmj-dashboard-viewer .lm_header {\n display: block !important;\n}\n\n/* Ensure GL root container fills available space */\nmj-dashboard-viewer .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-dashboard-viewer .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-dashboard-viewer .lm_item,\nmj-dashboard-viewer .lm_content,\nmj-dashboard-viewer .lm_stack,\nmj-dashboard-viewer .lm_row,\nmj-dashboard-viewer .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-dashboard-viewer .lm_item {\n overflow: hidden !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks (its height is set with the stack header below) */\nmj-dashboard-viewer .lm_items {\n width: 100% !important;\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-dashboard-viewer .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the ComponentItem div inside lm_items */\nmj-dashboard-viewer .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-dashboard-viewer .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout */\nmj-dashboard-viewer .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds */\nmj-dashboard-viewer .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_content {\n background: var(--mj-bg-surface) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-dashboard-viewer .lm_item_container {\n background: var(--mj-bg-surface) !important;\n}\n\n/* ===== Golden Layout: part tabs and part buttons =====\n The shell's tab-container and theme rules reach this nested layout. These selectors add\n .dashboard-viewer to outrank them, with !important where those rules use it. */\n\n/* Stack header: tab track on the left, part buttons on the right (DASHBOARD_STACK_HEADER_HEIGHT) */\nmj-dashboard-viewer .dashboard-viewer .lm_header {\n display: block !important;\n height: 44px !important;\n max-height: none !important; /* shell.component.css caps nested headers at 40px */\n padding: 0 !important;\n background: var(--mj-bg-surface) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n z-index: auto; /* no stacking context, so the tab dropdown list can rise above the part */\n}\nmj-dashboard-viewer .dashboard-viewer .lm_items {\n height: calc(100% - 44px) !important;\n}\n\n/* The tab track, styled like mj-tab-nav. Golden Layout counts each tab's width and right margin; the rest of the\n spacing beside the tabs is DASHBOARD_TAB_CONTROL_OFFSET (78px): the track's 12px inset, 1px borders and 4px left\n padding, an 8px gap, the 34px tab dropdown button and its 6px gap, and the controls' 12px inset. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabs {\n top: 5px !important;\n left: var(--mj-space-3) !important;\n right: auto !important; /* shell sets left:0; right:0; overflow-x:auto */\n height: auto !important;\n overflow: visible !important;\n padding: var(--mj-space-1) 0 var(--mj-space-1) var(--mj-space-1); /* the last tab's margin closes the right side */\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab {\n display: inline-flex !important;\n align-items: center !important;\n gap: var(--mj-space-2) !important;\n flex: 0 0 auto !important;\n height: auto !important;\n margin: 0 var(--mj-space-1) 0 0 !important;\n padding: 4px var(--mj-space-3) !important;\n font-family: inherit !important;\n font-size: 13px !important;\n font-weight: 500 !important;\n line-height: 1.2 !important;\n color: var(--mj-text-secondary) !important;\n background: transparent !important;\n border: none !important;\n border-radius: var(--mj-radius-md) !important;\n box-shadow: none !important;\n cursor: pointer !important;\n transition: background-color var(--mj-transition-fast), color var(--mj-transition-fast) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab::after { content: none !important; } /* shell pipe separators */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab:hover:not(.lm_active) {\n background: color-mix(in srgb, var(--mj-bg-surface) 70%, transparent) !important;\n color: var(--mj-text-primary) !important;\n}\n/* The shell's active tab rule is as specific as the tab rule above, so the tab's box is repeated here */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab.lm_active {\n height: auto !important;\n margin: 0 var(--mj-space-1) 0 0 !important;\n border: none !important;\n border-radius: var(--mj-radius-md) !important;\n background: var(--mj-bg-surface) !important;\n color: var(--mj-brand-primary) !important;\n box-shadow: var(--mj-shadow-sm) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n}\nmj-dashboard-viewer .dashboard-viewer.editing .lm_header .lm_tab { cursor: grab !important; }\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_title {\n cursor: inherit !important;\n line-height: normal !important;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tab .panel-icon { /* the part's icon */\n position: static !important;\n width: auto !important;\n height: auto !important;\n transform: none !important;\n font-size: 13px !important;\n color: inherit !important;\n}\n\n/* Parts are removed only through Remove (with a confirmation): hide GL's close buttons */\nmj-dashboard-viewer .dashboard-viewer .lm_close_tab,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_close,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_popout,\nmj-dashboard-viewer .dashboard-viewer .lm_controls .lm_maximise { display: none !important; }\n\n/* Part buttons (Edit part, Remove) in the header's controls area */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls {\n top: 0 !important;\n bottom: 0 !important;\n right: var(--mj-space-3) !important;\n z-index: 3; /* above tabs that run into it (the shell gives tabs 1 and 2), under the tab dropdown list */\n align-items: center;\n gap: 6px;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .dashboard-stack-actions {\n display: flex !important;\n align-items: center;\n gap: 6px;\n width: auto !important; /* GL base: .lm_controls > * is 18x18 and floats */\n height: auto !important;\n float: none !important;\n opacity: 1 !important; /* MJ theme: .lm_controls > * is at 0.6 */\n cursor: default !important;\n}\n\n/* Golden Layout's tab dropdown button, sized like the part buttons and placed left of them. It sits outside the\n controls' measured width, because Golden Layout shows it only after it measures the tabs. Golden Layout shows\n and hides it with an inline display, so display here carries no !important. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown {\n display: inline-flex;\n position: absolute;\n top: 0;\n bottom: 0;\n right: 100%;\n margin: auto 6px auto 0;\n width: 32px !important; /* GL base: .lm_controls > * is 18x18 and floats */\n height: 32px !important;\n float: none !important;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: var(--mj-radius-md);\n color: var(--mj-text-secondary);\n opacity: 1 !important; /* MJ theme: .lm_controls > * is at 0.6 */\n cursor: pointer;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown:hover {\n background-color: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_controls > .lm_tabdropdown::before {\n content: '\\f078'; /* chevron-down, in place of GL's border triangle */\n width: auto;\n height: auto;\n border: none;\n font-family: 'Font Awesome 6 Free';\n font-weight: 900;\n font-size: 12px;\n line-height: 1;\n color: inherit;\n}\n\n/* Tabs that do not fit: Golden Layout's tab dropdown list, as a menu below the header. Golden Layout shows and\n hides it with an inline display, so display here carries no !important. */\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list {\n display: flex;\n flex-direction: column;\n top: calc(100% + var(--mj-space-1));\n right: var(--mj-space-3);\n z-index: var(--mj-z-dropdown);\n min-width: 200px;\n max-height: 290px;\n overflow-y: auto;\n padding: var(--mj-space-1);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-md);\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_tab {\n display: flex !important;\n margin: 0 !important;\n padding: 6px var(--mj-space-2) !important;\n border-radius: var(--mj-radius-sm) !important;\n background: transparent !important;\n box-shadow: none !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_tab:hover:not(.lm_active) {\n background: var(--mj-bg-surface-hover) !important;\n}\nmj-dashboard-viewer .dashboard-viewer .lm_header .lm_tabdropdown_list .lm_title {\n width: auto; /* GL base: 100px */\n}\n\n/* Each stack (a panel of part tabs) is a card */\nmj-dashboard-viewer .dashboard-viewer .lm_stack {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default) !important;\n border-radius: var(--mj-radius-lg);\n}\n\n/* ========================================\n View Mode (Not Editing) - Lock Layout\n ======================================== */\n\n/* Disable splitter dragging when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter {\n pointer-events: none !important;\n cursor: default !important;\n}\n\n/* Hide splitter drag handle visual when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_splitter .lm_drag_handle {\n display: none !important;\n}\n\n/* Disable tab dragging cursor when not editing */\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_header .lm_tab {\n cursor: default !important;\n}\n\nmj-dashboard-viewer .dashboard-viewer:not(.editing) .lm_title {\n cursor: default !important;\n}\n"] }]
1479
1505
  }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }, { type: i0.Injector }, { type: i0.EnvironmentInjector }], { Dashboard: [{
1480
1506
  type: Input
1481
1507
  }], dashboard: [{
@@ -1488,6 +1514,8 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1488
1514
  type: Input
1489
1515
  }], isEditing: [{
1490
1516
  type: Input
1517
+ }], CanEdit: [{
1518
+ type: Input
1491
1519
  }], ShowToolbar: [{
1492
1520
  type: Input
1493
1521
  }], showToolbar: [{
@@ -1554,5 +1582,5 @@ export class DashboardViewerComponent extends BaseAngularComponent {
1554
1582
  type: ViewChild,
1555
1583
  args: ['layoutContainer', { static: true }]
1556
1584
  }] }); })();
1557
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardViewerComponent, { className: "DashboardViewerComponent", filePath: "src/lib/dashboard-viewer/dashboard-viewer.component.ts", lineNumber: 73 }); })();
1585
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardViewerComponent, { className: "DashboardViewerComponent", filePath: "src/lib/dashboard-viewer/dashboard-viewer.component.ts", lineNumber: 76 }); })();
1558
1586
  //# sourceMappingURL=dashboard-viewer.component.js.map