diff --git a/src/ui/src/components/home_page/home_page.ng.html b/src/ui/src/components/home_page/home_page.ng.html
index ee271ae3..0c58dcaf 100644
--- a/src/ui/src/components/home_page/home_page.ng.html
+++ b/src/ui/src/components/home_page/home_page.ng.html
@@ -107,7 +107,8 @@
(titleClicked)="handleClickTitle()"
(modelGraphProcessed)="handleModelGraphProcessed($event)"
(uiStateChanged)="handleUiStateChanged($event)"
- (remoteNodeDataPathsChanged)="handleRemoteNodeDataPathsChanged($event)">
+ (remoteNodeDataPathsChanged)="handleRemoteNodeDataPathsChanged($event)"
+ (syncNavigationModeChanged)="handleSyncNavigationModeChanged($event)">
diff --git a/src/ui/src/components/home_page/home_page.ts b/src/ui/src/components/home_page/home_page.ts
index 102032cf..9c334ff8 100644
--- a/src/ui/src/components/home_page/home_page.ts
+++ b/src/ui/src/components/home_page/home_page.ts
@@ -58,7 +58,10 @@ import {ModelSourceInput} from '../model_source_input/model_source_input';
import {OpenInNewTabButton} from '../open_in_new_tab_button/open_in_new_tab_button';
import {OpenSourceLibsDialog} from '../open_source_libs_dialog/open_source_libs_dialog';
import {SettingsDialog} from '../settings_dialog/settings_dialog';
-import {ModelGraphProcessedEvent} from '../visualizer/common/types';
+import {
+ ModelGraphProcessedEvent,
+ SyncNavigationModeChangedEvent,
+} from '../visualizer/common/types';
import {VisualizerConfig} from '../visualizer/common/visualizer_config';
import {VisualizerUiState} from '../visualizer/common/visualizer_ui_state';
import {Logo} from '../visualizer/logo';
@@ -111,6 +114,7 @@ export class HomePage implements AfterViewInit {
benchmark = false;
remoteNodeDataPaths: string[] = [];
remoteNodeDataTargetModels: string[] = [];
+ syncNavigation?: SyncNavigationModeChangedEvent;
hasUploadedModels = signal(false);
shareButtonTooltip: Signal = signal('');
@@ -162,6 +166,9 @@ export class HomePage implements AfterViewInit {
// Remote node data paths encoded in the url.
this.remoteNodeDataPaths = this.urlService.getNodeDataSources();
this.remoteNodeDataTargetModels = this.urlService.getNodeDataTargets();
+
+ // Sync navigation.
+ this.syncNavigation = this.urlService.getSyncNavigation();
}
ngAfterViewInit() {
@@ -276,12 +283,22 @@ export class HomePage implements AfterViewInit {
);
this.remoteProcessedNodeDataTargetModels.add(modelName);
}
+
+ if (this.syncNavigation) {
+ this.modelGraphVisualizer?.syncNavigationService.loadSyncNavigationDataFromEvent(
+ this.syncNavigation,
+ );
+ }
}
handleRemoteNodeDataPathsChanged(paths: string[]) {
this.urlService.setNodeDataSources(paths);
}
+ handleSyncNavigationModeChanged(event: SyncNavigationModeChangedEvent) {
+ this.urlService.setSyncNavigation(event);
+ }
+
handleClickShowThirdPartyLibraries() {
this.dialog.open(OpenSourceLibsDialog, {});
}
diff --git a/src/ui/src/components/visualizer/app_service.ts b/src/ui/src/components/visualizer/app_service.ts
index 4bc24044..a57ca882 100644
--- a/src/ui/src/components/visualizer/app_service.ts
+++ b/src/ui/src/components/visualizer/app_service.ts
@@ -106,7 +106,7 @@ export class AppService {
readonly doubleClickedNode = signal(undefined);
- testMode: boolean = false;
+ testMode = false;
private groupNodeChildrenCountThresholdFromUrl: string | null = null;
diff --git a/src/ui/src/components/visualizer/common/sync_navigation.ts b/src/ui/src/components/visualizer/common/sync_navigation.ts
new file mode 100644
index 00000000..d9ac981f
--- /dev/null
+++ b/src/ui/src/components/visualizer/common/sync_navigation.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2024 The Model Explorer Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ==============================================================================
+ */
+
+import {TaskData, TaskType} from './task';
+
+/** The data for navigation syncing. */
+export interface SyncNavigationData extends TaskData {
+ type: TaskType.SYNC_NAVIGATION;
+
+ mapping: SyncNavigationMapping;
+}
+
+/**
+ * The mapping for navigation syncing, from node id from one side to node id
+ * from another side.
+ */
+export type SyncNavigationMapping = Record;
+
+/** The mode of navigation syncing. */
+export enum SyncNavigationMode {
+ DISABLED = 'disabled',
+ MATCH_NODE_ID = 'match_node_id',
+ VISUALIZER_CONFIG = 'visualizer_config',
+ UPLOAD_MAPPING_FROM_COMPUTER = 'from_computer',
+ LOAD_MAPPING_FROM_CNS = 'from_cns',
+}
+
+/** The labels for sync navigation modes. */
+export const SYNC_NAVIGATION_MODE_LABELS = {
+ [SyncNavigationMode.DISABLED]: 'Disabled',
+ [SyncNavigationMode.MATCH_NODE_ID]: 'Match node id',
+ [SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER]:
+ 'Upload mapping from computer',
+ [SyncNavigationMode.LOAD_MAPPING_FROM_CNS]: 'Load mapping from CNS',
+ [SyncNavigationMode.VISUALIZER_CONFIG]: 'From Visualizer Config',
+};
+
+/** Information about the source of navigation. */
+export interface NavigationSourceInfo {
+ paneIndex: number;
+ nodeId: string;
+}
diff --git a/src/ui/src/components/visualizer/common/task.ts b/src/ui/src/components/visualizer/common/task.ts
new file mode 100644
index 00000000..ec2bcbd1
--- /dev/null
+++ b/src/ui/src/components/visualizer/common/task.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2024 The Model Explorer Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ==============================================================================
+ */
+
+/** The base data for a task. */
+export declare interface TaskData {
+ type: TaskType;
+}
+
+/** The type of a task. */
+export enum TaskType {
+ SYNC_NAVIGATION = 'sync_navigation',
+}
diff --git a/src/ui/src/components/visualizer/common/types.ts b/src/ui/src/components/visualizer/common/types.ts
index 377c1267..cb1f9c32 100644
--- a/src/ui/src/components/visualizer/common/types.ts
+++ b/src/ui/src/components/visualizer/common/types.ts
@@ -17,6 +17,7 @@
*/
import {GroupNode, ModelGraph, ModelNode} from './model_graph';
+import {SyncNavigationMode} from './sync_navigation';
/** A type for key-value pairs. */
export type KeyValuePairs = Record;
@@ -170,6 +171,7 @@ export interface SelectedNodeInfo {
rendererId: string;
isGroupNode: boolean;
noNodeShake?: boolean;
+ triggerNavigationSync?: boolean;
}
/** Info about a node to locate. */
@@ -658,3 +660,15 @@ export declare interface SerializedStyle {
id: NodeStyleId;
value: string;
}
+
+/** Response from reading a file. */
+export declare interface ReadFileResp {
+ content: string;
+}
+
+/** Event for sync navigation mode change. */
+export declare interface SyncNavigationModeChangedEvent {
+ mode: SyncNavigationMode;
+ // Used when mode is LOAD_MAPPING_FROM_CNS.
+ cnsPath?: string;
+}
diff --git a/src/ui/src/components/visualizer/common/visualizer_config.ts b/src/ui/src/components/visualizer/common/visualizer_config.ts
index adee7268..db47abd7 100644
--- a/src/ui/src/components/visualizer/common/visualizer_config.ts
+++ b/src/ui/src/components/visualizer/common/visualizer_config.ts
@@ -16,6 +16,7 @@
* ==============================================================================
*/
+import {SyncNavigationData} from './sync_navigation';
import {NodeStylerRule, RendererType} from './types';
/** Configs for the visualizer. */
@@ -59,6 +60,9 @@ export declare interface VisualizerConfig {
/** The default node styler rules. */
nodeStylerRules?: NodeStylerRule[];
+ /** The data for navigation syncing. */
+ syncNavigationData?: SyncNavigationData;
+
/**
* Default graph renderer.
*
diff --git a/src/ui/src/components/visualizer/common/worker_events.ts b/src/ui/src/components/visualizer/common/worker_events.ts
index c55acac4..2a6a390a 100644
--- a/src/ui/src/components/visualizer/common/worker_events.ts
+++ b/src/ui/src/components/visualizer/common/worker_events.ts
@@ -118,6 +118,7 @@ export declare interface RelayoutGraphRequest extends WorkerEventBase {
clearAllExpandStates?: boolean;
forRestoringSnapshotAfterTogglingFlattenLayers?: boolean;
nodeStylerQueries?: NodeStylerRule[];
+ triggerNavigationSync?: boolean;
}
/** The response for re-laying out the whole graph. */
@@ -130,6 +131,7 @@ export declare interface RelayoutGraphResponse extends WorkerEventBase {
rectToZoomFit?: Rect;
forRestoringSnapshotAfterTogglingFlattenLayers?: boolean;
targetDeepestGroupNodeIdsToExpand?: string[];
+ triggerNavigationSync?: boolean;
}
/** The request for locating a node. */
diff --git a/src/ui/src/components/visualizer/model_graph_visualizer.ts b/src/ui/src/components/visualizer/model_graph_visualizer.ts
index aa5b9c0f..d86c71d6 100644
--- a/src/ui/src/components/visualizer/model_graph_visualizer.ts
+++ b/src/ui/src/components/visualizer/model_graph_visualizer.ts
@@ -45,6 +45,7 @@ import {
NodeDataProviderData,
NodeDataProviderGraphData,
NodeInfo,
+ SyncNavigationModeChangedEvent,
} from './common/types';
import {genUid, inInputElement, isOpNode} from './common/utils';
import {type VisualizerConfig} from './common/visualizer_config';
@@ -53,6 +54,7 @@ import {ExtensionService} from './extension_service';
import {NodeDataProviderExtensionService} from './node_data_provider_extension_service';
import {NodeStylerService} from './node_styler_service';
import {SplitPanesContainer} from './split_panes_container';
+import {SyncNavigationService} from './sync_navigation_service';
import {ThreejsService} from './threejs_service';
import {TitleBar} from './title_bar';
import {UiStateService} from './ui_state_service';
@@ -70,6 +72,7 @@ import {WorkerService} from './worker_service';
ExtensionService,
NodeDataProviderExtensionService,
NodeStylerService,
+ SyncNavigationService,
UiStateService,
WorkerService,
],
@@ -109,6 +112,10 @@ export class ModelGraphVisualizer implements OnInit, OnDestroy, OnChanges {
/** Triggered when a remote node data paths are updated. */
@Output() readonly remoteNodeDataPathsChanged = new EventEmitter();
+ /** Triggered when the sync navigation mode is changed. */
+ @Output() readonly syncNavigationModeChanged =
+ new EventEmitter();
+
/** Triggered when the selected node is changed. */
@Output() readonly selectedNodeChanged = new EventEmitter();
@@ -140,6 +147,7 @@ export class ModelGraphVisualizer implements OnInit, OnDestroy, OnChanges {
private readonly uiStateService: UiStateService,
private readonly nodeDataProviderExtensionService: NodeDataProviderExtensionService,
private readonly nodeStylerService: NodeStylerService,
+ readonly syncNavigationService: SyncNavigationService,
) {
effect(() => {
@@ -201,6 +209,12 @@ export class ModelGraphVisualizer implements OnInit, OnDestroy, OnChanges {
this.modelGraphProcessed.next(event);
});
+ this.syncNavigationService.syncNavigationModeChanged$
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((event) => {
+ this.syncNavigationModeChanged.next(event);
+ });
+
this.initThreejs();
}
diff --git a/src/ui/src/components/visualizer/node_data_provider_extension_service.ts b/src/ui/src/components/visualizer/node_data_provider_extension_service.ts
index 2fa7d6e5..ca15a1de 100644
--- a/src/ui/src/components/visualizer/node_data_provider_extension_service.ts
+++ b/src/ui/src/components/visualizer/node_data_provider_extension_service.ts
@@ -28,6 +28,7 @@ import {
NodeDataProviderData,
NodeDataProviderResultProcessedData,
NodeDataProviderRunData,
+ ReadFileResp,
ThresholdItem,
} from './common/types';
import {genUid, isOpNode} from './common/utils';
@@ -44,10 +45,6 @@ interface ProcessedGradientItem {
textColor?: Rgb;
}
-declare interface ReadFileResp {
- content: string;
-}
-
declare interface ExternalReadFileResp {
content: string;
error?: string;
diff --git a/src/ui/src/components/visualizer/node_styler_service.ts b/src/ui/src/components/visualizer/node_styler_service.ts
index 8d5e014a..0f79e76f 100644
--- a/src/ui/src/components/visualizer/node_styler_service.ts
+++ b/src/ui/src/components/visualizer/node_styler_service.ts
@@ -110,23 +110,20 @@ export class NodeStylerService {
private readonly appService: AppService,
private readonly localStorageService: LocalStorageService,
) {
- effect(
- () => {
- const rules = this.rules();
+ effect(() => {
+ const rules = this.rules();
- if (!this.appService.testMode) {
- // Save rules to local storage on changes.
- this.localStorageService.setItem(
- LOCAL_STORAGE_KEY_NODE_STYLER_RULES,
- JSON.stringify(rules),
- );
- }
+ if (!this.appService.testMode) {
+ // Save rules to local storage on changes.
+ this.localStorageService.setItem(
+ LOCAL_STORAGE_KEY_NODE_STYLER_RULES,
+ JSON.stringify(rules),
+ );
+ }
- // Compute matched nodes.
- this.computeMatchedNodes(rules);
- },
- {allowSignalWrites: true},
- );
+ // Compute matched nodes.
+ this.computeMatchedNodes(rules);
+ });
// Load rules from local storage in non-test mode.
if (!this.appService.testMode) {
diff --git a/src/ui/src/components/visualizer/split_panes_container.ng.html b/src/ui/src/components/visualizer/split_panes_container.ng.html
index d3861008..516ab161 100644
--- a/src/ui/src/components/visualizer/split_panes_container.ng.html
+++ b/src/ui/src/components/visualizer/split_panes_container.ng.html
@@ -75,7 +75,9 @@
}
-
+
{{getPaneTitle(pane)}}
@@ -103,4 +105,12 @@
(mousedown)="handleMouseDownResizer($event, panesContainer)">
+
+
+ @if (hasSplitPane && allPanesLoaded()) {
+
+
+
+ }
diff --git a/src/ui/src/components/visualizer/split_panes_container.scss b/src/ui/src/components/visualizer/split_panes_container.scss
index 28dfcedc..633f3351 100644
--- a/src/ui/src/components/visualizer/split_panes_container.scss
+++ b/src/ui/src/components/visualizer/split_panes_container.scss
@@ -41,6 +41,14 @@
cursor: pointer;
flex-shrink: 0;
+ &.extra-left-padding {
+ padding-left: 36px;
+ }
+
+ &.extra-right-padding {
+ padding-right: 36px;
+ }
+
.buttons-container {
display: flex;
align-items: center;
@@ -65,6 +73,13 @@
width: 18px;
}
}
+
+ .divider {
+ width: 1px;
+ height: 12px;
+ background-color: #999;
+ margin: 0 4px 0 12px;
+ }
}
split-pane {
@@ -82,6 +97,10 @@
.pane-title-container {
background-color: #ea8600;
color: white;
+
+ .divider {
+ background-color: white;
+ }
}
}
@@ -193,6 +212,15 @@
border-left: 1px solid #999;
}
}
+
+ .sync-navigation-container {
+ position: absolute;
+ transform: translate(-22px, 0);
+ top: 0px;
+ height: 24px;
+ // Over resizer.
+ z-index: 250;
+ }
}
::ng-deep .model-explorer-processing-tasks-container {
diff --git a/src/ui/src/components/visualizer/split_panes_container.ts b/src/ui/src/components/visualizer/split_panes_container.ts
index 02c90607..24daf804 100644
--- a/src/ui/src/components/visualizer/split_panes_container.ts
+++ b/src/ui/src/components/visualizer/split_panes_container.ts
@@ -16,16 +16,18 @@
* ==============================================================================
*/
-import {animate, state, style, transition, trigger} from '@angular/animations';
+import {animate, style, transition, trigger} from '@angular/animations';
import {CommonModule} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ computed,
effect,
ElementRef,
QueryList,
+ Signal,
ViewChild,
ViewChildren,
} from '@angular/core';
@@ -48,6 +50,7 @@ import {
import {GraphPanel} from './graph_panel';
import {InfoPanel} from './info_panel';
import {SplitPane} from './split_pane';
+import {SyncNavigationButton} from './sync_navigation_button';
import {WorkerService} from './worker_service';
interface ProcessingTask {
@@ -69,6 +72,7 @@ interface ProcessingTask {
MatProgressSpinnerModule,
MatTooltipModule,
SplitPane,
+ SyncNavigationButton,
],
templateUrl: './split_panes_container.ng.html',
styleUrls: ['./split_panes_container.scss'],
@@ -90,6 +94,7 @@ export class SplitPanesContainer implements AfterViewInit {
@ViewChildren('splitPane') splitPanes = new QueryList();
readonly processingTasks: Record = {};
+ readonly allPanesLoaded: Signal;
resizingSplitPane = false;
curLeftWidthFraction = 1;
@@ -103,6 +108,10 @@ export class SplitPanesContainer implements AfterViewInit {
private readonly workerService: WorkerService,
) {
this.panes = this.appService.panes;
+ this.allPanesLoaded = computed(() =>
+ this.panes().every((pane) => pane.modelGraph != null),
+ );
+
effect(() => {
const panes = this.panes();
if (panes.length >= 1) {
diff --git a/src/ui/src/components/visualizer/sync_navigation_button.ng.html b/src/ui/src/components/visualizer/sync_navigation_button.ng.html
new file mode 100644
index 00000000..ad32f9cd
--- /dev/null
+++ b/src/ui/src/components/visualizer/sync_navigation_button.ng.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ {{syncIcon()}}
+
+
Sync
+
+
+
+
+
+
+
+
+
+
+ @for (mode of allSyncModes; track mode) {
+
+
+
+ {{getModeLabel(mode)}}
+
+ @switch (mode) {
+ @case (SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER) {
+
+
+
+ {{uploadedFileName}}
+
+ }
+ }
+
+
+ }
+
+
\ No newline at end of file
diff --git a/src/ui/src/components/visualizer/sync_navigation_button.scss b/src/ui/src/components/visualizer/sync_navigation_button.scss
new file mode 100644
index 00000000..a84e9b6d
--- /dev/null
+++ b/src/ui/src/components/visualizer/sync_navigation_button.scss
@@ -0,0 +1,190 @@
+/**
+ * @license
+ * Copyright 2024 The Model Explorer Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ==============================================================================
+ */
+
+$sync_highlight_color: #004fb8;
+
+@keyframes rotating {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.container {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ font-size: 11px;
+ cursor: pointer;
+ color: #777;
+ padding: 0 5px;
+ background-color: white;
+ border-radius: 99px;
+ border: 1px solid #ccc;
+ box-sizing: border-box;
+
+ .content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ &.enabled {
+ background-color: $sync_highlight_color;
+ color: white;
+
+ mat-icon {
+ color: white;
+ }
+ }
+
+ mat-icon {
+ font-size: 18px;
+ height: 18px;
+ width: 18px;
+
+ &.loading {
+ animation: rotating 2s linear infinite;
+ }
+ }
+}
+
+::ng-deep .model-explorer-sync-navigation-dropdown {
+ font-size: 12px;
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 12px;
+
+ .section-label {
+ padding: 8px 12px;
+ margin-bottom: 8px;
+ font-size: 11px;
+ background: #f1f1f1;
+ font-weight: 500;
+ text-transform: uppercase;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .right {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ .icon-container {
+ display: flex;
+ cursor: pointer;
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ mat-icon {
+ font-size: 18px;
+ height: 18px;
+ width: 18px;
+ color: #999;
+ }
+ }
+ }
+
+ .section {
+ padding-right: 16px;
+ }
+
+ mat-radio-button {
+ cursor: pointer;
+
+ &.cns {
+ margin-top: 8px;
+ }
+
+ > div[mat-internal-form-field] {
+ height: 24px;
+ }
+
+ div:has(> input[type='radio']) {
+ transform: scale(0.7);
+ margin-right: -8px;
+ }
+
+ label {
+ letter-spacing: normal;
+ cursor: pointer;
+ font-size: 12px;
+ font-family: 'Google Sans Text', 'Google Sans', Arial, Helvetica,
+ sans-serif;
+ }
+ }
+
+ .select-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .upload-mapping-button {
+ margin: 2px 0 0 36px;
+ width: 90px;
+ height: 30px;
+ /* stylelint-disable-next-line declaration-no-important -- override MDC */
+ font-size: 12px !important;
+ /* stylelint-disable-next-line declaration-no-important -- override MDC */
+ letter-spacing: normal !important;
+
+ &.cns {
+ margin-top: 4px;
+ }
+
+ ::ng-deep .mat-mdc-button-touch-target {
+ display: none;
+ }
+ }
+
+ .upload-mapping-input {
+ display: none;
+ }
+
+ .uploaded-file-name {
+ margin-left: 36px;
+ color: #999;
+ line-break: anywhere;
+ line-height: 14px;
+ }
+
+ textarea {
+ height: 48px;
+ box-sizing: border-box;
+ margin: 4px 0 0 36px;
+ resize: none;
+ border-radius: 3px;
+ font-family: 'Google Sans Text', 'Google Sans', Arial, Helvetica, sans-serif;
+ font-size: 11px;
+ padding: 2px;
+ line-break: anywhere;
+ }
+}
diff --git a/src/ui/src/components/visualizer/sync_navigation_button.ts b/src/ui/src/components/visualizer/sync_navigation_button.ts
new file mode 100644
index 00000000..7ae99888
--- /dev/null
+++ b/src/ui/src/components/visualizer/sync_navigation_button.ts
@@ -0,0 +1,190 @@
+/**
+ * @license
+ * Copyright 2024 The Model Explorer Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ==============================================================================
+ */
+
+import {OverlaySizeConfig} from '@angular/cdk/overlay';
+import {CommonModule} from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ ViewChild,
+ computed,
+ inject,
+} from '@angular/core';
+import {MatButtonModule} from '@angular/material/button';
+import {MatIconModule} from '@angular/material/icon';
+import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
+import {MatRadioModule} from '@angular/material/radio';
+import {MatSnackBar} from '@angular/material/snack-bar';
+import {MatTooltipModule} from '@angular/material/tooltip';
+
+import {Bubble} from '../bubble/bubble';
+import {BubbleClick} from '../bubble/bubble_click';
+
+import {AppService} from './app_service';
+import {
+ SYNC_NAVIGATION_MODE_LABELS,
+ SyncNavigationMode,
+} from './common/sync_navigation';
+import {LocalStorageService} from './local_storage_service';
+import {SyncNavigationService} from './sync_navigation_service';
+
+/** The button to manage sync navigation. */
+@Component({
+ standalone: true,
+ selector: 'sync-navigation-button',
+ imports: [
+ Bubble,
+ BubbleClick,
+ CommonModule,
+ MatButtonModule,
+ MatIconModule,
+ MatProgressSpinnerModule,
+ MatRadioModule,
+ MatTooltipModule,
+ ],
+ templateUrl: 'sync_navigation_button.ng.html',
+ styleUrls: ['./sync_navigation_button.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SyncNavigationButton {
+ @ViewChild(BubbleClick) dropdown?: BubbleClick;
+
+ private readonly appService = inject(AppService);
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly localStorageService = inject(LocalStorageService);
+ private readonly syncNavigationService = inject(SyncNavigationService);
+ private readonly snackBar = inject(MatSnackBar);
+
+ readonly SyncNavigationMode = SyncNavigationMode;
+ readonly allSyncModes: SyncNavigationMode[];
+ readonly syncMode = this.syncNavigationService.mode;
+ readonly syncEnabled = computed(() => {
+ return this.syncMode() !== SyncNavigationMode.DISABLED;
+ });
+ readonly syncIcon = computed(() =>
+ this.syncMode() === SyncNavigationMode.DISABLED &&
+ !this.syncNavigationService.loadingFromCns()
+ ? 'sync_disabled'
+ : 'sync',
+ );
+ readonly loadingFromCns = this.syncNavigationService.loadingFromCns;
+
+ readonly helpPopupSize: OverlaySizeConfig = {
+ minWidth: 0,
+ minHeight: 0,
+ };
+
+ readonly dropdownSize: OverlaySizeConfig = {
+ minWidth: 0,
+ minHeight: 0,
+ maxHeight: 500,
+ };
+
+ uploadedFileName = '';
+
+ constructor() {
+
+ // Populate sync modes.
+ //
+ // Show "component input" mode only if there is sync navigation data passed
+ // through visualizer config..
+ const syncNavigationDataFromVisConfig =
+ this.appService.config()?.syncNavigationData;
+ this.allSyncModes = syncNavigationDataFromVisConfig
+ ? [
+ SyncNavigationMode.DISABLED,
+ SyncNavigationMode.MATCH_NODE_ID,
+ SyncNavigationMode.VISUALIZER_CONFIG,
+ SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER,
+ SyncNavigationMode.LOAD_MAPPING_FROM_CNS,
+ ]
+ : [
+ SyncNavigationMode.DISABLED,
+ SyncNavigationMode.MATCH_NODE_ID,
+ SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER,
+ SyncNavigationMode.LOAD_MAPPING_FROM_CNS,
+ ];
+ // If there is sync navigation data passed through visualizer config, set
+ // the sync navigation data for the "visualizer config" mode and select the
+ // mode by default.
+ if (syncNavigationDataFromVisConfig) {
+ this.syncNavigationService.mode.set(SyncNavigationMode.VISUALIZER_CONFIG);
+ this.syncNavigationService.updateSyncNavigationData(
+ SyncNavigationMode.VISUALIZER_CONFIG,
+ syncNavigationDataFromVisConfig,
+ );
+ }
+ }
+
+ setSyncMode(mode: SyncNavigationMode) {
+ this.syncNavigationService.mode.set(mode);
+
+ switch (mode) {
+ case SyncNavigationMode.DISABLED:
+ case SyncNavigationMode.MATCH_NODE_ID:
+ this.syncNavigationService.syncNavigationModeChanged$.next({
+ mode,
+ });
+ break;
+ default:
+ break;
+ }
+ }
+
+ getModeLabel(mode: SyncNavigationMode): string {
+ return SYNC_NAVIGATION_MODE_LABELS[mode];
+ }
+
+ handleClickUpload(input: HTMLInputElement) {
+ this.syncNavigationService.mode.set(
+ SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER,
+ );
+ input.click();
+ }
+
+ handleUploadedFileChanged(input: HTMLInputElement) {
+ const files = input.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+ const file = files[0];
+ this.uploadedFileName = '';
+
+ const fileReader = new FileReader();
+ fileReader.onload = (event) => {
+ const error = this.syncNavigationService.processJsonData(
+ event.target?.result as string,
+ SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER,
+ );
+ if (!error) {
+ this.uploadedFileName = file.name;
+ this.changeDetectorRef.markForCheck();
+ }
+ };
+ fileReader.readAsText(file);
+ }
+
+ private showError(message: string) {
+ console.error(message);
+ this.snackBar.open(message, 'Dismiss', {
+ duration: 5000,
+ });
+ }
+}
diff --git a/src/ui/src/components/visualizer/sync_navigation_service.ts b/src/ui/src/components/visualizer/sync_navigation_service.ts
new file mode 100644
index 00000000..b3d66fa2
--- /dev/null
+++ b/src/ui/src/components/visualizer/sync_navigation_service.ts
@@ -0,0 +1,132 @@
+/**
+ * @license
+ * Copyright 2024 The Model Explorer Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ==============================================================================
+ */
+
+import {
+ NavigationSourceInfo,
+ SyncNavigationData,
+ SyncNavigationMode,
+} from './common/sync_navigation';
+import {ReadFileResp, SyncNavigationModeChangedEvent} from './common/types';
+
+import {Injectable, signal} from '@angular/core';
+import {Subject} from 'rxjs';
+
+declare interface ProcessedSyncNavigationData extends SyncNavigationData {
+ inversedMapping: Record;
+}
+
+/** A service for split pane sync navigation related tasks. */
+@Injectable()
+export class SyncNavigationService {
+ readonly mode = signal(SyncNavigationMode.DISABLED);
+ readonly navigationSourceChanged$ = new Subject();
+ readonly loadingFromCns = signal(false);
+
+ // Used for notifying mode change to other components.
+ readonly syncNavigationModeChanged$ =
+ new Subject();
+
+ private savedProcessedSyncNavigationData: Record<
+ string,
+ ProcessedSyncNavigationData
+ > = {};
+
+ updateNavigationSource(info: NavigationSourceInfo) {
+ if (this.mode() === SyncNavigationMode.DISABLED) {
+ return;
+ }
+ this.navigationSourceChanged$.next(info);
+ }
+
+ updateSyncNavigationData(mode: SyncNavigationMode, data: SyncNavigationData) {
+ // Generate inversed mapping.
+ const processedData: ProcessedSyncNavigationData = {
+ ...data,
+ inversedMapping: {},
+ };
+ for (const key of Object.keys(data.mapping)) {
+ processedData.inversedMapping[data.mapping[key]] = key;
+ }
+
+ // Save it.
+ this.savedProcessedSyncNavigationData[mode] = processedData;
+ }
+
+ getMappedNodeId(paneIndex: number, nodeId: string): string {
+ const mode = this.mode();
+ const curSyncNavigationData: ProcessedSyncNavigationData | undefined =
+ this.savedProcessedSyncNavigationData[mode];
+
+ switch (mode) {
+ case SyncNavigationMode.MATCH_NODE_ID: {
+ return nodeId;
+ }
+ case SyncNavigationMode.VISUALIZER_CONFIG:
+ case SyncNavigationMode.UPLOAD_MAPPING_FROM_COMPUTER:
+ case SyncNavigationMode.LOAD_MAPPING_FROM_CNS: {
+ // Get mapped node id from mapping.
+ // Fallback to the original node id if not found.
+ const mapping = curSyncNavigationData?.mapping ?? {};
+ const inversedMapping = curSyncNavigationData?.inversedMapping ?? {};
+ return mapping[nodeId] ?? inversedMapping[nodeId] ?? nodeId;
+ }
+ default:
+ return nodeId;
+ }
+ }
+
+ async loadFromCns(path: string): Promise {
+ // Call API to read file content.
+ this.loadingFromCns.set(true);
+ const url = `/read_file?path=${path}`;
+ const resp = await fetch(url);
+ if (!resp.ok) {
+ return `Failed to load JSON file "${path}"`;
+ }
+
+ // Parse response.
+ const json = JSON.parse(
+ (await resp.text()).replace(")]}'\n", ''),
+ ) as ReadFileResp;
+
+ const error = this.processJsonData(
+ json.content,
+ SyncNavigationMode.LOAD_MAPPING_FROM_CNS,
+ );
+
+ this.loadingFromCns.set(false);
+
+ return error;
+ }
+
+ async loadSyncNavigationDataFromEvent(event: SyncNavigationModeChangedEvent) {
+
+ // Set mode.
+ this.mode.set(event.mode);
+ }
+
+ processJsonData(str: string, mode: SyncNavigationMode): string {
+ try {
+ const data = JSON.parse(str) as SyncNavigationData;
+ this.updateSyncNavigationData(mode, data);
+ } catch (e) {
+ return `Failed to parse JSON file. ${e}`;
+ }
+ return '';
+ }
+}
diff --git a/src/ui/src/components/visualizer/webgl_renderer.ts b/src/ui/src/components/visualizer/webgl_renderer.ts
index d9b6e8de..d380e359 100644
--- a/src/ui/src/components/visualizer/webgl_renderer.ts
+++ b/src/ui/src/components/visualizer/webgl_renderer.ts
@@ -105,6 +105,7 @@ import {NodeDataProviderExtensionService} from './node_data_provider_extension_s
import {NodeStylerService} from './node_styler_service';
import {SplitPaneService} from './split_pane_service';
import {SubgraphSelectionService} from './subgraph_selection_service';
+import {SyncNavigationService} from './sync_navigation_service';
import {ThreejsService} from './threejs_service';
import {UiStateService} from './ui_state_service';
import {WebglEdges} from './webgl_edges';
@@ -398,6 +399,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
workerEvent.rectToZoomFit,
workerEvent.forRestoringSnapshotAfterTogglingFlattenLayers,
workerEvent.targetDeepestGroupNodeIdsToExpand,
+ workerEvent.triggerNavigationSync,
);
}
break;
@@ -440,6 +442,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
private readonly snackBar: MatSnackBar,
private readonly splitPaneService: SplitPaneService,
private readonly subgraphSelectionService: SubgraphSelectionService,
+ private readonly syncNavigationService: SyncNavigationService,
private readonly uiStateService: UiStateService,
private readonly viewContainerRef: ViewContainerRef,
private readonly webglRendererAttrsTableService: WebglRendererAttrsTableService,
@@ -484,45 +487,39 @@ export class WebglRenderer implements OnInit, OnDestroy {
});
// Handle changes for node to locate.
- effect(
- () => {
- const nodeInfoToLocate = this.appService.curToLocateNodeInfo();
- if (nodeInfoToLocate?.rendererId !== this.rendererId) {
- return;
- }
+ effect(() => {
+ const nodeInfoToLocate = this.appService.curToLocateNodeInfo();
+ if (nodeInfoToLocate?.rendererId !== this.rendererId) {
+ return;
+ }
- if (nodeInfoToLocate) {
- this.sendLocateNodeRequest(
- nodeInfoToLocate.nodeId,
- nodeInfoToLocate.rendererId,
- nodeInfoToLocate.noNodeShake,
- nodeInfoToLocate.select,
- );
- }
- this.appService.curToLocateNodeInfo.set(undefined);
- },
- {allowSignalWrites: true},
- );
+ if (nodeInfoToLocate) {
+ this.sendLocateNodeRequest(
+ nodeInfoToLocate.nodeId,
+ nodeInfoToLocate.rendererId,
+ nodeInfoToLocate.noNodeShake,
+ nodeInfoToLocate.select,
+ );
+ }
+ this.appService.curToLocateNodeInfo.set(undefined);
+ });
// Handle changes for node to reveal
- effect(
- () => {
- const pane = this.appService.getPaneById(this.paneId);
- if (!pane || !pane.modelGraph) {
- return;
- }
+ effect(() => {
+ const pane = this.appService.getPaneById(this.paneId);
+ if (!pane || !pane.modelGraph) {
+ return;
+ }
- const nodeIdToReveal = pane.nodeIdToReveal;
- if (!nodeIdToReveal) {
- return;
- }
- const success = this.revealNode(nodeIdToReveal);
- if (success) {
- this.appService.setNodeToReveal(this.paneId, undefined);
- }
- },
- {allowSignalWrites: true},
- );
+ const nodeIdToReveal = pane.nodeIdToReveal;
+ if (!nodeIdToReveal) {
+ return;
+ }
+ const success = this.revealNode(nodeIdToReveal);
+ if (success) {
+ this.appService.setNodeToReveal(this.paneId, undefined);
+ }
+ });
effect(() => {
const runs = this.nodeDataProviderExtensionService.getRunsForModelGraph(
@@ -585,7 +582,9 @@ export class WebglRenderer implements OnInit, OnDestroy {
return;
}
- this.selectedNodeId = info?.nodeId || '';
+ const selectedNodeId = info?.nodeId || '';
+ const selectedNodeChanged = this.selectedNodeId !== selectedNodeId;
+ this.selectedNodeId = selectedNodeId;
if (this.tracing) {
if (
@@ -604,6 +603,14 @@ export class WebglRenderer implements OnInit, OnDestroy {
this.webglRendererIdenticalLayerService.updateIdenticalLayerIndicators();
this.updateNodesStyles();
this.webglRendererThreejsService.render();
+
+ // Trigger a navigation sync request (if enabled).
+ if (selectedNodeChanged && info.triggerNavigationSync) {
+ this.syncNavigationService.updateNavigationSource({
+ paneIndex: this.appService.getPaneIndexById(this.paneId) || 0,
+ nodeId: this.selectedNodeId,
+ });
+ }
});
// Handle "download as png".
@@ -674,6 +681,26 @@ export class WebglRenderer implements OnInit, OnDestroy {
this.updateNodesStyles();
this.webglRendererThreejsService.render();
});
+
+ // Handle navigation sync.
+ this.syncNavigationService.navigationSourceChanged$
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((data) => {
+ if (!data) {
+ return;
+ }
+
+ if (data.paneIndex !== this.appService.getPaneIndexById(this.paneId)) {
+ const mappedNodeId = this.syncNavigationService.getMappedNodeId(
+ data.paneIndex,
+ data.nodeId,
+ );
+ const mappedNode = this.curModelGraph.nodesById[mappedNodeId];
+ if (mappedNode && mappedNode.id !== this.selectedNodeId) {
+ this.revealNode(mappedNodeId, false);
+ }
+ }
+ });
}
ngOnInit() {
@@ -730,6 +757,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
true,
snapshot.showOnNodeItemTypes,
true,
+ false,
);
pane.snapshotToRestore = undefined;
} else {
@@ -784,13 +812,18 @@ export class WebglRenderer implements OnInit, OnDestroy {
deepestExpandedGroupNodeIds = groupNodeIds;
}
if (
- paneState.selectedNodeId != '' ||
+ paneState.selectedNodeId !== '' ||
deepestExpandedGroupNodeIds.length > 0
) {
this.sendRelayoutGraphRequest(
paneState.selectedNodeId,
deepestExpandedGroupNodeIds,
true,
+ undefined,
+ false,
+ undefined,
+ false,
+ false,
);
} else {
initGraphFn();
@@ -1022,10 +1055,8 @@ export class WebglRenderer implements OnInit, OnDestroy {
return;
}
this.handleSelectNode(this.hoveredNodeId);
- this.handleToggleExpandCollapse(
- this.curModelGraph.nodesById[this.hoveredNodeId],
- all,
- );
+ const node = this.curModelGraph.nodesById[this.hoveredNodeId] as GroupNode;
+ this.handleToggleExpandCollapse(node, all);
}
handleClickExpandAll(nodeId?: string) {
@@ -1161,16 +1192,16 @@ export class WebglRenderer implements OnInit, OnDestroy {
// Expand/collapse node on double click. Alt key controls whether to do it
// for all sub layers.
if (this.selectedNodeId !== '' && !shiftDown) {
+ const node = this.curModelGraph.nodesById[
+ this.selectedNodeId
+ ] as GroupNode;
this.appService.updateDoubleClickedNode(
this.selectedNodeId,
this.curModelGraph.id,
this.curModelGraph.collectionLabel || '',
- this.curModelGraph.nodesById[this.selectedNodeId],
- );
- this.handleToggleExpandCollapse(
- this.curModelGraph.nodesById[this.selectedNodeId],
- altDown,
+ node,
);
+ this.handleToggleExpandCollapse(node, altDown);
}
}
@@ -1287,6 +1318,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
clearAllExpandStates = false,
showOnNodeItemTypes?: Record,
forRestoringSnapshotAfterTogglingFlattenLayers?: boolean,
+ triggerNavigationSync = true,
) {
this.showBusySpinnerWithDelay();
@@ -1302,6 +1334,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
rectToZoomFit,
clearAllExpandStates,
forRestoringSnapshotAfterTogglingFlattenLayers,
+ triggerNavigationSync,
};
this.workerService.worker.postMessage(req);
}
@@ -1544,7 +1577,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
return this.webglRendererThreejsService.fps;
}
- private handleSelectNode(nodeId: string) {
+ private handleSelectNode(nodeId: string, triggerNavigationSync = true) {
this.appService.selectNode(this.paneId, {
nodeId,
rendererId: this.rendererId,
@@ -1552,6 +1585,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
nodeId === ''
? false
: isGroupNode(this.curModelGraph.nodesById[nodeId]),
+ triggerNavigationSync,
});
}
@@ -1626,6 +1660,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
rectToZoomFit?: Rect,
forRestoringSnapshotAfterTogglingFlattenLayers?: boolean,
targetDeepestGroupNodeIdsToExpand?: string[],
+ triggerNavigationSync?: boolean,
) {
this.updateCurModelGraph(modelGraph);
this.updateNodesAndEdgesToRender();
@@ -1662,7 +1697,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
// Select node.
if (this.selectedNodeId !== selectedNodeId) {
- this.handleSelectNode(selectedNodeId || '');
+ this.handleSelectNode(selectedNodeId || '', triggerNavigationSync);
}
if (!this.inPopup) {
@@ -2762,7 +2797,7 @@ export class WebglRenderer implements OnInit, OnDestroy {
this.changeDetectorRef.detectChanges();
}
- private revealNode(nodeId: string): boolean {
+ private revealNode(nodeId: string, triggerNavigationSync = true): boolean {
const node = this.curModelGraph.nodesById[nodeId];
if (!node) {
return false;
@@ -2770,6 +2805,12 @@ export class WebglRenderer implements OnInit, OnDestroy {
this.sendRelayoutGraphRequest(
nodeId,
node.nsParentId ? [node.nsParentId] : [],
+ false,
+ undefined,
+ false,
+ undefined,
+ false,
+ triggerNavigationSync,
);
return true;
}
diff --git a/src/ui/src/components/visualizer/worker/worker.ts b/src/ui/src/components/visualizer/worker/worker.ts
index 8029bf82..ac78beec 100644
--- a/src/ui/src/components/visualizer/worker/worker.ts
+++ b/src/ui/src/components/visualizer/worker/worker.ts
@@ -161,6 +161,7 @@ self.addEventListener('message', (event: Event) => {
workerEvent.forRestoringSnapshotAfterTogglingFlattenLayers,
targetDeepestGroupNodeIdsToExpand:
workerEvent.targetDeepestGroupNodeIdsToExpand,
+ triggerNavigationSync: workerEvent.triggerNavigationSync,
};
postMessage(resp);
break;
diff --git a/src/ui/src/services/url_service.ts b/src/ui/src/services/url_service.ts
index 85330a70..b3d8313f 100644
--- a/src/ui/src/services/url_service.ts
+++ b/src/ui/src/services/url_service.ts
@@ -19,6 +19,7 @@
import {Injectable} from '@angular/core';
import {Params, Router} from '@angular/router';
+import {SyncNavigationModeChangedEvent} from '../components/visualizer/common/types';
import {VisualizerUiState} from '../components/visualizer/common/visualizer_ui_state';
/** All URL query parameter keys. */
@@ -43,6 +44,7 @@ declare interface OldEncodedUrlData {
declare interface EncodedUrlData {
models: ModelSource[];
nodeData?: string[];
+ sync?: SyncNavigationModeChangedEvent;
// Target model names (e.g. model.tflite) that each of the `nodeData` above
// is applied to.
nodeDataTargets?: string[];
@@ -65,6 +67,7 @@ export declare interface ModelSource {
export class UrlService {
private models: ModelSource[] = [];
private nodeData?: string[] = [];
+ private syncNavigation?: SyncNavigationModeChangedEvent;
private nodeDataTargets?: string[] = [];
private uiState?: VisualizerUiState;
private prevQueryParamStr = '';
@@ -107,6 +110,15 @@ export class UrlService {
this.updateUrl();
}
+ getSyncNavigation(): SyncNavigationModeChangedEvent | undefined {
+ return this.syncNavigation;
+ }
+
+ setSyncNavigation(syncNavigation: SyncNavigationModeChangedEvent) {
+ this.syncNavigation = syncNavigation;
+ this.updateUrl();
+ }
+
getNodeDataTargets(): string[] {
return this.nodeDataTargets || [];
}
@@ -125,6 +137,7 @@ export class UrlService {
nodeData: this.nodeData,
nodeDataTargets: this.nodeDataTargets,
uiState: this.uiState,
+ sync: this.syncNavigation,
};
queryParams[QueryParamKey.DATA] = JSON.stringify(data);
queryParams[QueryParamKey.RENDERER] = this.renderer;
@@ -194,6 +207,7 @@ export class UrlService {
this.models = decodedData.models;
this.uiState = decodedData.uiState;
this.nodeData = decodedData.nodeData;
+ this.syncNavigation = decodedData.sync;
this.nodeDataTargets = decodedData.nodeDataTargets;
}
diff --git a/src/ui/src/theme/theme.scss b/src/ui/src/theme/theme.scss
index 3c22ece9..660ef403 100644
--- a/src/ui/src/theme/theme.scss
+++ b/src/ui/src/theme/theme.scss
@@ -106,6 +106,7 @@ $blue-palette: (
@include mat.list-theme($theme);
@include mat.menu-theme($theme);
@include mat.progress-spinner-theme($theme);
+ @include mat.radio-theme($theme);
@include mat.select-theme($theme);
@include mat.sidenav-theme($theme);
@include mat.slide-toggle-theme($theme);