diff --git a/src/ui/src/components/visualizer/common/types.ts b/src/ui/src/components/visualizer/common/types.ts
index cb1f9c32..b346d184 100644
--- a/src/ui/src/components/visualizer/common/types.ts
+++ b/src/ui/src/components/visualizer/common/types.ts
@@ -236,8 +236,24 @@ export declare interface NodeInfo {
node?: ModelNode;
}
+/** Supported aggregated stats. */
+export type AggregatedStat = 'min' | 'max' | 'sum' | 'avg';
+
/** Node data provider data for a single graph. */
export declare interface NodeDataProviderGraphData {
+ /**
+ * The name of the node data.
+ *
+ * The node data's name (mainly used for display purposes) is determined by
+ * the following sources, in order of priority:
+ *
+ * 1) an explicitly specified name from this field, which overrides any other
+ * source;
+ * 2) the `name` parameter in the API call, if applicable;
+ * 3) the JSON file name, if the data is loaded from a JSON file.
+ */
+ name?: string;
+
/**
* Node data indexed by node keys.
*
@@ -282,7 +298,28 @@ export declare interface NodeDataProviderGraphData {
*/
gradient?: GradientItem[];
- // https://gist.github.com/Myndex/e1025706436736166561d339fd667493
+ /**
+ * Whether to hide the corresponding column in aggregated stats table
+ * (the first table).
+ *
+ * If all columns in that table are hidden, the whole table will be hidden.
+ */
+ hideInAggregatedStatsTable?: boolean;
+
+ /**
+ * Whether to hide the corresponding column in children stats table
+ * (the second table).
+ *
+ * If all columns in that table are hidden, the whole table will be hidden.
+ */
+ hideInChildrenStatsTable?: boolean;
+
+ /**
+ * The stats to hide in the aggregated stats table (the first table).
+ *
+ * The value for the hidden stat will be displayed as '-'.
+ */
+ hideAggregatedStats?: AggregatedStat[];
}
/** The top level node data provider data, indexed by graph id. */
diff --git a/src/ui/src/components/visualizer/common/utils.ts b/src/ui/src/components/visualizer/common/utils.ts
index 6c2a9963..f321c464 100644
--- a/src/ui/src/components/visualizer/common/utils.ts
+++ b/src/ui/src/components/visualizer/common/utils.ts
@@ -541,11 +541,14 @@ export function getOpNodeDataProviderKeyValuePairsForAttrsTable(
type.replace(NODE_DATA_PROVIDER_SHOW_ON_NODE_TYPE_PREFIX, ''),
);
const runs = Object.values(curNodeDataProviderRuns).filter((run) =>
- runNames.includes(run.runName),
+ runNames.includes(getRunName(run, {id: modelGraphId})),
);
for (const run of runs) {
const value = (run.results || {})?.[modelGraphId][node.id]?.strValue || '-';
- keyValuePairs.push({key: run.runName, value});
+ keyValuePairs.push({
+ key: getRunName(run, {id: modelGraphId}),
+ value,
+ });
}
return keyValuePairs;
}
@@ -1053,3 +1056,13 @@ export function getIntersectionPoints(rect1: Rect, rect2: Rect) {
return {intersection1, intersection2};
}
+
+/** Gets the run name for the given run. */
+export function getRunName(
+ run: NodeDataProviderRunData,
+ modelGraphIdLike?: {id: string},
+): string {
+ return (
+ run.nodeDataProviderData?.[modelGraphIdLike?.id || '']?.name ?? run.runName
+ );
+}
diff --git a/src/ui/src/components/visualizer/info_panel.ts b/src/ui/src/components/visualizer/info_panel.ts
index ae746074..e3769570 100644
--- a/src/ui/src/components/visualizer/info_panel.ts
+++ b/src/ui/src/components/visualizer/info_panel.ts
@@ -53,7 +53,12 @@ import {
SearchMatchType,
SearchResults,
} from './common/types';
-import {getNamespaceLabel, isGroupNode, isOpNode} from './common/utils';
+import {
+ getNamespaceLabel,
+ getRunName,
+ isGroupNode,
+ isOpNode,
+} from './common/utils';
import {ExpandableInfoText} from './expandable_info_text';
import {HoverableLabel} from './hoverable_label';
import {InfoPanelService} from './info_panel_service';
@@ -751,7 +756,7 @@ export class InfoPanel {
nodeDataProvidersSection.items.push({
id: run.runId,
section: nodeDataProvidersSection,
- label: run.runName,
+ label: getRunName(run, this.curModelGraph),
value: strValue,
canShowOnNode: run.done,
showOnNode: this.curShowOnNodeDataProviderRuns[run.runId] != null,
diff --git a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html
index 0b3d1747..d4e60cbc 100644
--- a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html
+++ b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html
@@ -51,108 +51,116 @@
-
-
-
-
{{statsTableTitleIcon}}
- {{statsTableTitle}}
+ @if (showStatsTable) {
+
+
+
+ {{statsTableTitleIcon}}
+ {{statsTableTitle}}
+
+
+
+
+
+ Stat
+ |
+
+
+ |
+
+
+
+
+ {{row.stat}} |
+
+ {{getStatValue(value)}}
+ |
+
+
+
-
-
-
-
- Stat
- |
-
-
- |
-
-
-
-
- {{row.stat}} |
-
- {{getStatValue(value)}}
- |
-
-
-
-
+ }
-
-
-
-
{{childrenStatsTableTitleIcon}}
- {{childrenStatsTableTitle}}
+ @if (showChildrenStatsTable) {
+
+
+
+ {{childrenStatsTableTitleIcon}}
+ {{childrenStatsTableTitle}}
+
+ @if (childrenStatRowsCount > tablePageSize && !childrenStatsTableCollapsed) {
+
+
+ }
- @if (childrenStatRowsCount > tablePageSize && !childrenStatsTableCollapsed) {
-
-
- }
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ {{row.index}} |
+
+ {{row.label}}
+ |
+
+ {{strValue}}
+ |
+
+
+
-
-
-
-
-
-
-
- |
-
-
-
- |
-
-
-
-
- {{row.index}} |
-
- {{row.label}}
- |
-
- {{strValue}}
- |
-
-
-
-
+ }
stat.sum);
this.curStatRows[3].values = stats.map((stat) => stat.sum / stat.count);
+ // Hide stat values based on hideAggregatedStats.
+ const allStats: AggregatedStat[] = ['min', 'max', 'sum', 'avg'];
+ for (let i = 0; i < runs.length; i++) {
+ const run = runs[i];
+ const statsToHide: AggregatedStat[] =
+ run.nodeDataProviderData?.[this.curModelGraph.id]
+ ?.hideAggregatedStats ?? [];
+ for (let j = 0; j < allStats.length; j++) {
+ const stat = allStats[j];
+ if (statsToHide.includes(stat)) {
+ // Set the value to positive infinity so that it will be displayed as
+ // '-' in the table. See `getStatValue()`.
+ this.curStatRows[j].values[i] = Number.POSITIVE_INFINITY;
+ }
+ }
+ }
+
// Generate children stats columns.
this.childrenStatsCols = [];
let childrenStatColIndex = 0;
@@ -650,7 +721,10 @@ export class NodeDataProviderSummaryPanel implements OnChanges {
this.childrenStatsCols.push({
colIndex: childrenStatColIndex,
runIndex: i,
- label: `${runs[i].runName} • ${childrenStat}`,
+ label: `${this.getRunName(runs[i])} • ${childrenStat}`,
+ hideInChildrenStatsTable:
+ runs[i].nodeDataProviderData?.[this.curModelGraph.id]
+ ?.hideInChildrenStatsTable,
});
childrenStatColIndex++;
}
@@ -667,6 +741,7 @@ export class NodeDataProviderSummaryPanel implements OnChanges {
const node = this.curModelGraph.nodesById[nodeId];
const colValues: number[] = [];
const colStrs: string[] = [];
+ const colHidden: boolean[] = [];
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
const run = runs[runIndex];
const curResults = run.results || {};
@@ -697,6 +772,10 @@ export class NodeDataProviderSummaryPanel implements OnChanges {
}
colValues.push(sumPct);
colStrs.push(hasValue ? sumPct.toFixed(1) : '-');
+ colHidden.push(
+ run.nodeDataProviderData?.[this.curModelGraph.id]
+ ?.hideInChildrenStatsTable === true,
+ );
}
this.curChildrenStatRows.push({
id: nodeId,
@@ -704,6 +783,7 @@ export class NodeDataProviderSummaryPanel implements OnChanges {
index: i,
colValues,
colStrs,
+ colHidden,
});
}
this.savedChildrenStatRows = [...this.curChildrenStatRows];
@@ -850,4 +930,8 @@ export class NodeDataProviderSummaryPanel implements OnChanges {
})
.join(',');
}
+
+ private getRunName(run: NodeDataProviderRunData): string {
+ return getRunName(run, this.curModelGraph);
+ }
}
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 516ab161..b4dcd8d5 100644
--- a/src/ui/src/components/visualizer/split_panes_container.ng.html
+++ b/src/ui/src/components/visualizer/split_panes_container.ng.html
@@ -111,6 +111,9 @@
+
+ No mapped node found
+
}
diff --git a/src/ui/src/components/visualizer/split_panes_container.scss b/src/ui/src/components/visualizer/split_panes_container.scss
index 633f3351..ee7fd7aa 100644
--- a/src/ui/src/components/visualizer/split_panes_container.scss
+++ b/src/ui/src/components/visualizer/split_panes_container.scss
@@ -220,6 +220,28 @@
height: 24px;
// Over resizer.
z-index: 250;
+
+ .no-mapped-node-message {
+ position: absolute;
+ top: 28px;
+ width: 140px;
+ font-size: 12px;
+ left: -44px;
+ background-color: #a00;
+ color: white;
+ padding: 2px 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 99px;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 100ms;
+
+ &.show {
+ opacity: 1;
+ }
+ }
}
}
diff --git a/src/ui/src/components/visualizer/split_panes_container.ts b/src/ui/src/components/visualizer/split_panes_container.ts
index 24daf804..644614c1 100644
--- a/src/ui/src/components/visualizer/split_panes_container.ts
+++ b/src/ui/src/components/visualizer/split_panes_container.ts
@@ -24,6 +24,7 @@ import {
ChangeDetectorRef,
Component,
computed,
+ DestroyRef,
effect,
ElementRef,
QueryList,
@@ -31,6 +32,7 @@ import {
ViewChild,
ViewChildren,
} from '@angular/core';
+import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MatIconModule} from '@angular/material/icon';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatTooltipModule} from '@angular/material/tooltip';
@@ -51,6 +53,7 @@ import {GraphPanel} from './graph_panel';
import {InfoPanel} from './info_panel';
import {SplitPane} from './split_pane';
import {SyncNavigationButton} from './sync_navigation_button';
+import {SyncNavigationService} from './sync_navigation_service';
import {WorkerService} from './worker_service';
interface ProcessingTask {
@@ -91,6 +94,8 @@ interface ProcessingTask {
})
export class SplitPanesContainer implements AfterViewInit {
@ViewChild('panesContainer') panesContainer!: ElementRef
;
+ @ViewChild('noMappedNodeMessage')
+ noMappedNodeMessage?: ElementRef;
@ViewChildren('splitPane') splitPanes = new QueryList();
readonly processingTasks: Record = {};
@@ -102,9 +107,13 @@ export class SplitPanesContainer implements AfterViewInit {
curUpdateProcessingProgressReq?: UpdateProcessingProgressRequest;
+ private hideNoMappedNodeMessageTimeoutId = -1;
+
constructor(
private readonly changeDetectorRef: ChangeDetectorRef,
private readonly appService: AppService,
+ private readonly destroyRef: DestroyRef,
+ private readonly syncNavigationService: SyncNavigationService,
private readonly workerService: WorkerService,
) {
this.panes = this.appService.panes;
@@ -142,6 +151,16 @@ export class SplitPanesContainer implements AfterViewInit {
break;
}
});
+
+ this.syncNavigationService.showNoMappedNodeMessageTrigger$
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((data) => {
+ if (data === undefined) {
+ this.hideNoMappedNodeMessage();
+ } else {
+ this.showNoMappedNodeMessage();
+ }
+ });
}
ngAfterViewInit() {
@@ -296,4 +315,36 @@ export class SplitPanesContainer implements AfterViewInit {
this.changeDetectorRef.detectChanges();
}
}
+
+ private hideNoMappedNodeMessage() {
+ const ele = this.noMappedNodeMessage?.nativeElement;
+ if (!ele) {
+ return;
+ }
+
+ if (this.hideNoMappedNodeMessageTimeoutId >= 0) {
+ clearTimeout(this.hideNoMappedNodeMessageTimeoutId);
+ this.hideNoMappedNodeMessageTimeoutId = -1;
+ }
+
+ ele.classList.remove('show');
+ }
+
+ private showNoMappedNodeMessage() {
+ const ele = this.noMappedNodeMessage?.nativeElement;
+ if (!ele) {
+ return;
+ }
+
+ if (this.hideNoMappedNodeMessageTimeoutId >= 0) {
+ clearTimeout(this.hideNoMappedNodeMessageTimeoutId);
+ this.hideNoMappedNodeMessageTimeoutId = -1;
+ }
+
+ // Hide after 3 seconds.
+ ele.classList.add('show');
+ this.hideNoMappedNodeMessageTimeoutId = setTimeout(() => {
+ ele.classList.remove('show');
+ }, 3000);
+ }
}
diff --git a/src/ui/src/components/visualizer/sync_navigation_service.ts b/src/ui/src/components/visualizer/sync_navigation_service.ts
index a9d5dcf8..af64156a 100644
--- a/src/ui/src/components/visualizer/sync_navigation_service.ts
+++ b/src/ui/src/components/visualizer/sync_navigation_service.ts
@@ -41,6 +41,9 @@ export class SyncNavigationService {
readonly syncNavigationModeChanged$ =
new Subject();
+ // {} means showing the message, and undefined means hiding the message.
+ readonly showNoMappedNodeMessageTrigger$ = new Subject<{} | undefined>();
+
private savedProcessedSyncNavigationData: Record<
string,
ProcessedSyncNavigationData
diff --git a/src/ui/src/components/visualizer/view_on_node.ts b/src/ui/src/components/visualizer/view_on_node.ts
index b58cf3e2..eaa770c9 100644
--- a/src/ui/src/components/visualizer/view_on_node.ts
+++ b/src/ui/src/components/visualizer/view_on_node.ts
@@ -45,6 +45,7 @@ import {
ShowOnNodeItemData,
ShowOnNodeItemType,
} from './common/types';
+import {getRunName} from './common/utils';
import {LocalStorageService} from './local_storage_service';
import {NodeDataProviderExtensionService} from './node_data_provider_extension_service';
@@ -101,7 +102,7 @@ export class ViewOnNode {
),
)
: [];
- return runs.map((run) => run.runName);
+ return runs.map((run) => getRunName(run, modelGraph));
});
private savedNodeDataProviderRunNames: string[] = [];
diff --git a/src/ui/src/components/visualizer/webgl_renderer.ts b/src/ui/src/components/visualizer/webgl_renderer.ts
index 689840ef..db63e52d 100644
--- a/src/ui/src/components/visualizer/webgl_renderer.ts
+++ b/src/ui/src/components/visualizer/webgl_renderer.ts
@@ -752,6 +752,19 @@ export class WebglRenderer implements OnInit, OnDestroy {
!hideInLayout
) {
this.revealNode(mappedNodeId, false);
+ this.syncNavigationService.showNoMappedNodeMessageTrigger$.next(
+ undefined,
+ );
+ } else {
+ if (mappedNodeId !== '' && (!mappedNode || hideInLayout)) {
+ this.syncNavigationService.showNoMappedNodeMessageTrigger$.next(
+ {},
+ );
+ } else if (mappedNodeId === '') {
+ this.syncNavigationService.showNoMappedNodeMessageTrigger$.next(
+ undefined,
+ );
+ }
}
}
});
diff --git a/src/ui/src/components/visualizer/webgl_renderer_io_highlight_service.ts b/src/ui/src/components/visualizer/webgl_renderer_io_highlight_service.ts
index 853ffc65..fa370ec0 100644
--- a/src/ui/src/components/visualizer/webgl_renderer_io_highlight_service.ts
+++ b/src/ui/src/components/visualizer/webgl_renderer_io_highlight_service.ts
@@ -901,9 +901,9 @@ export class WebglRendererIoHighlightService {
targetNodeId: string,
): ModelEdge | undefined {
const groupNodeId = namespace === '' ? '' : `${namespace}___group___`;
- return this.webglRenderer.curModelGraph.edgesByGroupNodeIds[
- groupNodeId
- ].find((edge) => {
+ return (
+ this.webglRenderer.curModelGraph.edgesByGroupNodeIds[groupNodeId] ?? []
+ ).find((edge) => {
const fromNode =
this.webglRenderer.curModelGraph.nodesById[edge.fromNodeId];
const toNode = this.webglRenderer.curModelGraph.nodesById[edge.toNodeId];
diff --git a/src/ui/src/components/visualizer/worker/graph_layout.ts b/src/ui/src/components/visualizer/worker/graph_layout.ts
index b845226b..dcc6f2ea 100644
--- a/src/ui/src/components/visualizer/worker/graph_layout.ts
+++ b/src/ui/src/components/visualizer/worker/graph_layout.ts
@@ -55,6 +55,7 @@ import {
getOpNodeFieldLabelsFromShowOnNodeItemTypes,
getOpNodeInputsKeyValuePairsForAttrsTable,
getOpNodeOutputsKeyValuePairsForAttrsTable,
+ getRunName,
isGroupNode,
isOpNode,
splitLabel,
@@ -493,6 +494,7 @@ export function getNodeHeight(
showOnNodeItemTypes,
node,
nodeDataProviderRuns,
+ modelGraph,
);
} else if (isGroupNode(node)) {
attrsTableRowCount = getGroupNodeAttrsTableRowCount(
@@ -583,6 +585,7 @@ function getOpNodeAttrsTableRowCount(
showOnNodeItemTypes: Record,
node: OpNode,
nodeDataProviderRuns: Record,
+ modelGraph: ModelGraph,
): number {
// Basic info fields.
const baiscFieldIds =
@@ -623,7 +626,7 @@ function getOpNodeAttrsTableRowCount(
) &&
Object.values(nodeDataProviderRuns).some(
(run) =>
- run.runName ===
+ getRunName(run, modelGraph) ===
showOnNodeItemType.replace(
NODE_DATA_PROVIDER_SHOW_ON_NODE_TYPE_PREFIX,
'',