diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container.ts index ee11d252cd1..1460c773547 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container.ts @@ -18,7 +18,7 @@ import { OnDestroy, OnInit, } from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import {debuggerLoaded, debuggerUnloaded} from './actions'; import {getActiveRunId, getDebuggerRunListing} from './store'; import {State} from './store/debugger_types'; @@ -29,9 +29,9 @@ import {State} from './store/debugger_types'; selector: 'tf-debugger-v2', template: ` `, styles: [ @@ -44,22 +44,20 @@ import {State} from './store/debugger_types'; ], }) export class DebuggerContainer implements OnInit, OnDestroy { - readonly runs$; + readonly runs; - readonly runsIds$; + readonly runsIds; - readonly activeRunId$; + readonly activeRunId; constructor(private readonly store: Store) { - this.runs$ = this.store.pipe(select(getDebuggerRunListing)); - this.runsIds$ = this.store.pipe( - select( - createSelector(getDebuggerRunListing, (runs): string[] => - Object.keys(runs) - ) + this.runs = this.store.selectSignal(getDebuggerRunListing); + this.runsIds = this.store.selectSignal( + createSelector(getDebuggerRunListing, (runs): string[] => + Object.keys(runs) ) ); - this.activeRunId$ = this.store.pipe(select(getActiveRunId)); + this.activeRunId = this.store.selectSignal(getActiveRunId); } ngOnInit(): void { diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container.ts index c814ebdd317..dcc9200052c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import {alertTypeFocusToggled} from '../../actions'; import { getAlertsBreakdown, @@ -49,9 +49,9 @@ const ALERT_TYPE_TO_DISPLAY_NAME_AND_SYMBOL: { selector: 'tf-debugger-v2-alerts', template: ` @@ -59,30 +59,28 @@ const ALERT_TYPE_TO_DISPLAY_NAME_AND_SYMBOL: { changeDetection: ChangeDetectionStrategy.OnPush, }) export class AlertsContainer { - readonly numAlerts$; + readonly numAlerts; - readonly alertsBreakdown$; + readonly alertsBreakdown; - readonly focusType$; + readonly focusType; constructor(private readonly store: Store) { - this.numAlerts$ = this.store.pipe(select(getNumAlerts)); - this.alertsBreakdown$ = this.store.pipe( - select( - createSelector(getAlertsBreakdown, (alertsBreakdown) => { - const alertTypes = Object.keys(alertsBreakdown); - alertTypes.sort(); - return alertTypes.map((alertType): AlertTypeDisplay => { - return { - type: alertType as AlertType, - ...ALERT_TYPE_TO_DISPLAY_NAME_AND_SYMBOL[alertType], - count: alertsBreakdown[alertType], - }; - }); - }) - ) + this.numAlerts = this.store.selectSignal(getNumAlerts); + this.alertsBreakdown = this.store.selectSignal( + createSelector(getAlertsBreakdown, (alertsBreakdown) => { + const alertTypes = Object.keys(alertsBreakdown); + alertTypes.sort(); + return alertTypes.map((alertType): AlertTypeDisplay => { + return { + type: alertType as AlertType, + ...ALERT_TYPE_TO_DISPLAY_NAME_AND_SYMBOL[alertType], + count: alertsBreakdown[alertType], + }; + }); + }) ); - this.focusType$ = this.store.pipe(select(getAlertsFocusType)); + this.focusType = this.store.selectSignal(getAlertsFocusType); } onToggleFocusType(alertType: AlertType) { diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_component.ts index ea07d7883c9..c33c6b7430e 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_component.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_component.ts @@ -28,7 +28,7 @@ export class ExecutionDataComponent { focusedExecutionIndex!: number; @Input() - focusedExecutionData!: Execution; + focusedExecutionData!: Execution | null; @Input() tensorDebugMode: TensorDebugMode = TensorDebugMode.UNSPECIFIED; diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_container.ts index c8db0d280be..ba90a089811 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data/execution_data_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import {getFocusedExecutionData} from '../../store'; import {Execution, State, TensorDebugMode} from '../../store/debugger_types'; import {DTYPE_ENUM_TO_NAME} from '../../tf_dtypes'; @@ -27,11 +27,11 @@ const UNKNOWN_DTYPE_NAME = 'Unknown dtype'; template: ` `, }) @@ -39,103 +39,84 @@ export class ExecutionDataContainer { @Input() focusedExecutionIndex!: number; - readonly focusedExecutionData$; + readonly focusedExecutionData; - readonly tensorDebugMode$; + readonly tensorDebugMode; - readonly hasDebugTensorValues$; + readonly hasDebugTensorValues; - readonly debugTensorValues$; + readonly debugTensorValues; - readonly debugTensorDtypes$; + readonly debugTensorDtypes; constructor(private readonly store: Store) { - this.focusedExecutionData$ = this.store.pipe( - select(getFocusedExecutionData) + this.focusedExecutionData = this.store.selectSignal( + getFocusedExecutionData ); - this.tensorDebugMode$ = this.store.pipe( - select( - createSelector( - getFocusedExecutionData, - (execution: Execution | null) => { - if (execution === null) { - return TensorDebugMode.UNSPECIFIED; - } else { - return execution.tensor_debug_mode; - } - } - ) - ) + this.tensorDebugMode = this.store.selectSignal( + createSelector(getFocusedExecutionData, (execution: Execution | null) => { + if (execution === null) { + return TensorDebugMode.UNSPECIFIED; + } else { + return execution.tensor_debug_mode; + } + }) ); - this.hasDebugTensorValues$ = this.store.pipe( - select( - createSelector( - getFocusedExecutionData, - (execution: Execution | null) => { - if (execution === null || execution.debug_tensor_values === null) { - return false; - } else { - for (const singleDebugTensorValues of execution.debug_tensor_values) { - if ( - singleDebugTensorValues !== null && - singleDebugTensorValues.length > 0 - ) { - return true; - } - } - return false; + this.hasDebugTensorValues = this.store.selectSignal( + createSelector(getFocusedExecutionData, (execution: Execution | null) => { + if (execution === null || execution.debug_tensor_values === null) { + return false; + } else { + for (const singleDebugTensorValues of execution.debug_tensor_values) { + if ( + singleDebugTensorValues !== null && + singleDebugTensorValues.length > 0 + ) { + return true; } } - ) - ) + return false; + } + }) ); - this.debugTensorValues$ = this.store.pipe( - select( - createSelector( - getFocusedExecutionData, - (execution: Execution | null) => { - if (execution === null) { - return null; - } else { - return execution.debug_tensor_values; - } - } - ) - ) + this.debugTensorValues = this.store.selectSignal( + createSelector(getFocusedExecutionData, (execution: Execution | null) => { + if (execution === null) { + return null; + } else { + return execution.debug_tensor_values; + } + }) ); - this.debugTensorDtypes$ = this.store.pipe( - select( - createSelector( - getFocusedExecutionData, - (execution: Execution | null): string[] | null => { - if (execution === null || execution.debug_tensor_values === null) { - return null; - } - if ( - execution.tensor_debug_mode !== TensorDebugMode.FULL_HEALTH && - execution.tensor_debug_mode !== TensorDebugMode.SHAPE - ) { - // TODO(cais): Add logic for other TensorDebugModes with dtype info. - return null; - } - const dtypes: string[] = []; - for (const tensorValue of execution.debug_tensor_values) { - if (tensorValue === null) { - dtypes.push(UNKNOWN_DTYPE_NAME); - } else { - const dtypeEnum = String( - execution.tensor_debug_mode === TensorDebugMode.FULL_HEALTH - ? tensorValue[2] // tensor_debug_mode: FULL_HEALTH - : tensorValue[1] // tensor_debug_mode: SHAPE - ); - dtypes.push( - DTYPE_ENUM_TO_NAME[dtypeEnum] || UNKNOWN_DTYPE_NAME - ); - } + this.debugTensorDtypes = this.store.selectSignal( + createSelector( + getFocusedExecutionData, + (execution: Execution | null): string[] | null => { + if (execution === null || execution.debug_tensor_values === null) { + return null; + } + if ( + execution.tensor_debug_mode !== TensorDebugMode.FULL_HEALTH && + execution.tensor_debug_mode !== TensorDebugMode.SHAPE + ) { + // TODO(cais): Add logic for other TensorDebugModes with dtype info. + return null; + } + const dtypes: string[] = []; + for (const tensorValue of execution.debug_tensor_values) { + if (tensorValue === null) { + dtypes.push(UNKNOWN_DTYPE_NAME); + } else { + const dtypeEnum = String( + execution.tensor_debug_mode === TensorDebugMode.FULL_HEALTH + ? tensorValue[2] // tensor_debug_mode: FULL_HEALTH + : tensorValue[1] // tensor_debug_mode: SHAPE + ); + dtypes.push(DTYPE_ENUM_TO_NAME[dtypeEnum] || UNKNOWN_DTYPE_NAME); } - return dtypes; } - ) + return dtypes; + } ) ); } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ng.html index 2b3e1df36c4..acf7005b54a 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ng.html @@ -20,10 +20,13 @@ - 0; else noInputs" class="inputs-container"> + 0; else noInputs" + class="inputs-container" + > Input slot {{slot}}: @@ -63,7 +66,7 @@ > diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ts index 4c979b80218..31b7ce91481 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_component.ts @@ -35,13 +35,13 @@ import { }) export class GraphComponent { @Input() - opInfo!: GraphOpInfo; + opInfo!: GraphOpInfo | null; @Input() - inputOps!: GraphOpInputSpec[]; + inputOps!: GraphOpInputSpec[] | null; @Input() - consumerOps!: GraphOpConsumerSpec[][]; + consumerOps!: GraphOpConsumerSpec[][] | null; @Output() onGraphOpNavigate = new EventEmitter<{graph_id: string; op_name: string}>(); @@ -50,14 +50,14 @@ export class GraphComponent { * Get the ID of the immediately-enclosing graph of the op. */ get graphId() { - return this.opInfo.graph_ids[this.opInfo.graph_ids.length - 1]; + return this.opInfo!.graph_ids[this.opInfo!.graph_ids.length - 1]; } /** * Total number of consumers of all output tensors of the op. */ get totalNumConsumers() { - return this.consumerOps.reduce((count, slotConsumers) => { + return this.consumerOps!.reduce((count, slotConsumers) => { return count + slotConsumers.length; }, 0); } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_container.ts index e080c8e28b8..af746d2c2cd 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph/graph_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {select, Store} from '@ngrx/store'; +import {Store} from '@ngrx/store'; import {graphOpFocused} from '../../actions'; import { getFocusedGraphOpConsumers, @@ -28,27 +28,27 @@ import {State} from '../../store/debugger_types'; selector: 'tf-debugger-v2-graph', template: ` `, }) export class GraphContainer { - readonly opInfo$; + readonly opInfo; - readonly inputOps$; + readonly inputOps; - readonly consumerOps$; + readonly consumerOps; onGraphOpNavigate(event: {graph_id: string; op_name: string}) { this.store.dispatch(graphOpFocused(event)); } constructor(private readonly store: Store) { - this.opInfo$ = this.store.pipe(select(getFocusedGraphOpInfo)); - this.inputOps$ = this.store.pipe(select(getFocusedGraphOpInputs)); - this.consumerOps$ = this.store.pipe(select(getFocusedGraphOpConsumers)); + this.opInfo = this.store.selectSignal(getFocusedGraphOpInfo); + this.inputOps = this.store.selectSignal(getFocusedGraphOpInputs); + this.consumerOps = this.store.selectSignal(getFocusedGraphOpConsumers); } } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html index c423447eb21..d8747e4e8b2 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html @@ -27,7 +27,7 @@ (scrolledIndexChange)="onScrolledIndexChange.emit($event)" > `, }) export class GraphExecutionsContainer { - readonly numGraphExecutions$; + readonly numGraphExecutions; - readonly graphExecutionData$; + readonly graphExecutionData; - readonly graphExecutionIndices$; + readonly graphExecutionIndices; - readonly focusIndex$; + readonly focusIndex; /** * Inferred graph-execution indices that belong to the immediate inputs * to the currently-focused graph op. */ - readonly focusInputIndices$; + readonly focusInputIndices; onScrolledIndexChange(scrolledIndex: number) { this.store.dispatch(graphExecutionScrollToIndex({index: scrolledIndex})); @@ -66,24 +66,22 @@ export class GraphExecutionsContainer { } constructor(private readonly store: Store) { - this.numGraphExecutions$ = this.store.pipe(select(getNumGraphExecutions)); - this.graphExecutionData$ = this.store.pipe(select(getGraphExecutionData)); - this.graphExecutionIndices$ = this.store.pipe( - select( - createSelector( - getNumGraphExecutions, - (numGraphExecution: number): number[] | null => { - if (numGraphExecution === 0) { - return null; - } - return Array.from({length: numGraphExecution}).map((_, i) => i); + this.numGraphExecutions = this.store.selectSignal(getNumGraphExecutions); + this.graphExecutionData = this.store.selectSignal(getGraphExecutionData); + this.graphExecutionIndices = this.store.selectSignal( + createSelector( + getNumGraphExecutions, + (numGraphExecution: number): number[] | null => { + if (numGraphExecution === 0) { + return null; } - ) + return Array.from({length: numGraphExecution}).map((_, i) => i); + } ) ); - this.focusIndex$ = this.store.pipe(select(getGraphExecutionFocusIndex)); - this.focusInputIndices$ = this.store.pipe( - select(getFocusedGraphExecutionInputIndices) + this.focusIndex = this.store.selectSignal(getGraphExecutionFocusIndex); + this.focusInputIndices = this.store.selectSignal( + getFocusedGraphExecutionInputIndices ); } } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts index ce7b5d779c4..a3e58f00df0 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts @@ -14,7 +14,6 @@ limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {State as OtherAppState} from '../../../../../webapp/app_state'; import {getDarkModeEnabled} from '../../../../../webapp/selectors'; import { @@ -29,24 +28,26 @@ import {State as DebuggerState} from '../../store/debugger_types'; selector: 'tf-debugger-v2-source-files', template: ` `, }) export class SourceFilesContainer { constructor(private readonly store: Store) { - this.focusedSourceFileContent$ = this.store.select( + this.focusedSourceFileContent = this.store.selectSignal( getFocusedSourceFileContent ); - this.focusedSourceLineSpec$ = this.store.select(getFocusedSourceLineSpec); - this.useDarkMode$ = this.store.select(getDarkModeEnabled); + this.focusedSourceLineSpec = this.store.selectSignal( + getFocusedSourceLineSpec + ); + this.useDarkMode = this.store.selectSignal(getDarkModeEnabled); } - readonly focusedSourceFileContent$; + readonly focusedSourceFileContent; - readonly focusedSourceLineSpec$; + readonly focusedSourceLineSpec; - readonly useDarkMode$: Observable; + readonly useDarkMode; } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts index 546affeb693..396bd534123 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import {sourceLineFocused} from '../../actions'; import { getCodeLocationOrigin, @@ -30,116 +30,102 @@ import {StackFrameForDisplay} from './stack_trace_component'; selector: 'tf-debugger-v2-stack-trace', template: ` `, }) export class StackTraceContainer { - readonly codeLocationType$; + readonly codeLocationType; - readonly opType$; + readonly opType; - readonly opName$; + readonly opName; - readonly executionIndex$; + readonly executionIndex; - readonly stickToBottommostFrameInFocusedFile$; + readonly stickToBottommostFrameInFocusedFile; - readonly stackFramesForDisplay$; + readonly stackFramesForDisplay; constructor(private readonly store: Store) { - this.codeLocationType$ = this.store.pipe( - select( - createSelector( - getCodeLocationOrigin, - (originInfo): CodeLocationType | null => { - return originInfo === null ? null : originInfo.codeLocationType; - } - ) + this.codeLocationType = this.store.selectSignal( + createSelector( + getCodeLocationOrigin, + (originInfo): CodeLocationType | null => { + return originInfo === null ? null : originInfo.codeLocationType; + } ) ); - this.opType$ = this.store.pipe( - select( - createSelector(getCodeLocationOrigin, (originInfo): string | null => { - return originInfo === null ? null : originInfo.opType; - }) - ) + this.opType = this.store.selectSignal( + createSelector(getCodeLocationOrigin, (originInfo): string | null => { + return originInfo === null ? null : originInfo.opType; + }) ); - this.opName$ = this.store.pipe( - select( - createSelector(getCodeLocationOrigin, (originInfo): string | null => { - if ( - originInfo === null || - originInfo.codeLocationType !== CodeLocationType.GRAPH_OP_CREATION - ) { - return null; - } - return originInfo.opName; - }) - ) + this.opName = this.store.selectSignal( + createSelector(getCodeLocationOrigin, (originInfo): string | null => { + if ( + originInfo === null || + originInfo.codeLocationType !== CodeLocationType.GRAPH_OP_CREATION + ) { + return null; + } + return originInfo.opName; + }) ); - this.executionIndex$ = this.store.pipe( - select( - createSelector(getCodeLocationOrigin, (originInfo): number | null => { - if ( - originInfo === null || - originInfo.codeLocationType !== CodeLocationType.EXECUTION - ) { - return null; - } - return originInfo.executionIndex; - }) - ) + this.executionIndex = this.store.selectSignal( + createSelector(getCodeLocationOrigin, (originInfo): number | null => { + if ( + originInfo === null || + originInfo.codeLocationType !== CodeLocationType.EXECUTION + ) { + return null; + } + return originInfo.executionIndex; + }) ); - this.stickToBottommostFrameInFocusedFile$ = this.store.pipe( - select(getStickToBottommostFrameInFocusedFile) + this.stickToBottommostFrameInFocusedFile = this.store.selectSignal( + getStickToBottommostFrameInFocusedFile ); - this.stackFramesForDisplay$ = this.store.pipe( - select( - createSelector( - getFocusedStackFrames, - getFocusedSourceLineSpec, - ( - stackFrames, - focusedSourceLineSpec - ): StackFrameForDisplay[] | null => { - if (stackFrames === null) { - return null; - } - const output: StackFrameForDisplay[] = []; - // Correctly label all the stack frames for display. - for (const stackFrame of stackFrames) { - const {host_name, file_path, lineno, function_name} = stackFrame; - const pathItems = file_path.split('/'); - const concise_file_path = pathItems[pathItems.length - 1]; - const belongsToFocusedFile = - focusedSourceLineSpec !== null && - host_name === focusedSourceLineSpec.host_name && - file_path === focusedSourceLineSpec.file_path; - const focused = - belongsToFocusedFile && - lineno === focusedSourceLineSpec!.lineno; - output.push({ - host_name, - file_path, - concise_file_path, - lineno, - function_name, - belongsToFocusedFile, - focused, - }); - } - return output; + this.stackFramesForDisplay = this.store.selectSignal( + createSelector( + getFocusedStackFrames, + getFocusedSourceLineSpec, + (stackFrames, focusedSourceLineSpec): StackFrameForDisplay[] | null => { + if (stackFrames === null) { + return null; + } + const output: StackFrameForDisplay[] = []; + // Correctly label all the stack frames for display. + for (const stackFrame of stackFrames) { + const {host_name, file_path, lineno, function_name} = stackFrame; + const pathItems = file_path.split('/'); + const concise_file_path = pathItems[pathItems.length - 1]; + const belongsToFocusedFile = + focusedSourceLineSpec !== null && + host_name === focusedSourceLineSpec.host_name && + file_path === focusedSourceLineSpec.file_path; + const focused = + belongsToFocusedFile && lineno === focusedSourceLineSpec!.lineno; + output.push({ + host_name, + file_path, + concise_file_path, + lineno, + function_name, + belongsToFocusedFile, + focused, + }); } - ) + return output; + } ) ); } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_container.ts index 464d8b35eab..28a0eb48f9e 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import { executionDigestFocused, executionScrollLeft, @@ -89,17 +89,17 @@ function getExecutionDigestForDisplay( selector: 'tf-debugger-v2-timeline', template: ` ) { - this.activeRunId$ = this.store.pipe(select(getActiveRunId)); - this.loadingNumExecutions$ = this.store.pipe( - select( - createSelector(getNumExecutionsLoaded, (loaded) => { - return loaded.state == DataLoadState.LOADING; - }) - ) + this.activeRunId = this.store.selectSignal(getActiveRunId); + this.loadingNumExecutions = this.store.selectSignal( + createSelector(getNumExecutionsLoaded, (loaded) => { + return loaded.state == DataLoadState.LOADING; + }) ); - this.scrollBeginIndex$ = this.store.pipe( - select(getExecutionScrollBeginIndex) + this.scrollBeginIndex = this.store.selectSignal( + getExecutionScrollBeginIndex ); - this.scrollBeginIndexUpperLimit$ = this.store.pipe( - select( - createSelector( - getNumExecutions, - getDisplayCount, - (numExecutions, displayCount) => { - return Math.max(0, numExecutions - displayCount); - } - ) + this.scrollBeginIndexUpperLimit = this.store.selectSignal( + createSelector( + getNumExecutions, + getDisplayCount, + (numExecutions, displayCount) => { + return Math.max(0, numExecutions - displayCount); + } ) ); - this.pageSize$ = this.store.pipe(select(getExecutionPageSize)); - this.displayCount$ = this.store.pipe(select(getDisplayCount)); - this.displayExecutionDigests$ = this.store.pipe( - select( - createSelector(getVisibleExecutionDigests, (visibleDigests) => { - return visibleDigests.map((digest) => - getExecutionDigestForDisplay(digest) - ); - }) - ) + this.pageSize = this.store.selectSignal(getExecutionPageSize); + this.displayCount = this.store.selectSignal(getDisplayCount); + this.displayExecutionDigests = this.store.selectSignal( + createSelector(getVisibleExecutionDigests, (visibleDigests) => { + return visibleDigests.map((digest) => + getExecutionDigestForDisplay(digest) + ); + }) ); - this.displayFocusedAlertTypes$ = this.store.pipe( - select(getFocusAlertTypesOfVisibleExecutionDigests) + this.displayFocusedAlertTypes = this.store.selectSignal( + getFocusAlertTypesOfVisibleExecutionDigests ); - this.focusedExecutionIndex$ = this.store.pipe( - select(getFocusedExecutionIndex) + this.focusedExecutionIndex = this.store.selectSignal( + getFocusedExecutionIndex ); - this.focusedExecutionDisplayIndex$ = this.store.pipe( - select(getFocusedExecutionDisplayIndex) + this.focusedExecutionDisplayIndex = this.store.selectSignal( + getFocusedExecutionDisplayIndex ); - this.numExecutions$ = this.store.pipe(select(getNumExecutions)); + this.numExecutions = this.store.selectSignal(getNumExecutions); } onNavigateLeft() { diff --git a/tensorboard/webapp/app_routing/views/router_outlet_container.ts b/tensorboard/webapp/app_routing/views/router_outlet_container.ts index 0e56b6acaac..a5059aecb98 100644 --- a/tensorboard/webapp/app_routing/views/router_outlet_container.ts +++ b/tensorboard/webapp/app_routing/views/router_outlet_container.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {combineLatest} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -29,33 +30,36 @@ import { selector: 'router-outlet', template: ` `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class RouterOutletContainer { - activeNgComponent$; + activeNgComponent; constructor( private readonly store: Store, private readonly registry: RouteRegistryModule ) { - this.activeNgComponent$ = combineLatest([ - this.store.select(getActiveRoute), - this.store.select(getNextRouteForRouterOutletOnly), - ]).pipe( - map(([activeRoute, nextRoute]) => { - if (!activeRoute) { - return null; - } - const isRouteTransitioning = - nextRoute !== null && - !areSameRouteKindAndExperiments(activeRoute, nextRoute); - return isRouteTransitioning - ? null - : this.registry.getNgComponentByRouteKind(activeRoute.routeKind); - }) + this.activeNgComponent = toSignal( + combineLatest([ + this.store.select(getActiveRoute), + this.store.select(getNextRouteForRouterOutletOnly), + ]).pipe( + map(([activeRoute, nextRoute]) => { + if (!activeRoute) { + return null; + } + const isRouteTransitioning = + nextRoute !== null && + !areSameRouteKindAndExperiments(activeRoute, nextRoute); + return isRouteTransitioning + ? null + : this.registry.getNgComponentByRouteKind(activeRoute.routeKind); + }) + ), + {requireSync: true} ); } } diff --git a/tensorboard/webapp/core/views/hash_storage_container.ts b/tensorboard/webapp/core/views/hash_storage_container.ts index 9c58707ef30..b9b6aa1e92c 100644 --- a/tensorboard/webapp/core/views/hash_storage_container.ts +++ b/tensorboard/webapp/core/views/hash_storage_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {select, Store} from '@ngrx/store'; +import {Store} from '@ngrx/store'; import {pluginUrlHashChanged} from '../actions'; import {State} from '../state'; import {getActivePlugin} from '../store'; @@ -24,7 +24,7 @@ import {ChangedProp} from './hash_storage_component'; selector: 'hash-storage', template: ` @@ -39,10 +39,10 @@ import {ChangedProp} from './hash_storage_component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class HashStorageContainer { - readonly activePluginId$; + readonly activePluginId; constructor(private readonly store: Store) { - this.activePluginId$ = this.store.pipe(select(getActivePlugin)); + this.activePluginId = this.store.selectSignal(getActivePlugin); } onValueChanged(change: {prop: ChangedProp; value: string}) { diff --git a/tensorboard/webapp/core/views/layout_container.ts b/tensorboard/webapp/core/views/layout_container.ts index 990058810d1..1b8f9b45817 100644 --- a/tensorboard/webapp/core/views/layout_container.ts +++ b/tensorboard/webapp/core/views/layout_container.ts @@ -17,9 +17,11 @@ import { Component, ElementRef, OnDestroy, + Signal, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {fromEvent, Observable, Subject} from 'rxjs'; +import {fromEvent, Subject} from 'rxjs'; import {combineLatestWith, filter, map, takeUntil} from 'rxjs/operators'; import {MouseEventButtons} from '../../util/dom'; import {runsTableFullScreenToggled, sideBarWidthChanged} from '../actions'; @@ -34,43 +36,41 @@ import { selector: 'tb-dashboard-layout', template: ` 0" + *ngIf="width() > 0" class="sidebar" - [style.width.%]="width$ | async" + [style.width.%]="width()" [style.minWidth.px]="MINIMUM_SIDEBAR_WIDTH_IN_PX" - [style.maxWidth.%]="(runsTableFullScreen$ | async) ? 100 : ''" + [style.maxWidth.%]="runsTableFullScreen() ? 100 : ''" > 0" + *ngIf="width() > 0" class="resizer" (mousedown)="resizeGrabbed()" > @@ -80,20 +80,23 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class LayoutContainer implements OnDestroy { - readonly runsTableFullScreen$; - readonly width$: Observable; + readonly runsTableFullScreen; + readonly width: Signal; private readonly ngUnsubscribe; private resizing: boolean = false; readonly MINIMUM_SIDEBAR_WIDTH_IN_PX = 75; constructor(private readonly store: Store, hostElRef: ElementRef) { - this.runsTableFullScreen$ = this.store.select(getRunsTableFullScreen); - this.width$ = this.store.select(getSideBarWidthInPercent).pipe( - combineLatestWith(this.runsTableFullScreen$), - map(([percentageWidth, fullScreen]) => { - return fullScreen ? 100 : percentageWidth; - }) + this.runsTableFullScreen = this.store.selectSignal(getRunsTableFullScreen); + this.width = toSignal( + this.store.select(getSideBarWidthInPercent).pipe( + combineLatestWith(this.store.select(getRunsTableFullScreen)), + map(([percentageWidth, fullScreen]) => { + return fullScreen ? 100 : percentageWidth; + }) + ), + {requireSync: true} ); this.ngUnsubscribe = new Subject(); fromEvent(hostElRef.nativeElement, 'mousemove') diff --git a/tensorboard/webapp/core/views/page_title_container.ts b/tensorboard/webapp/core/views/page_title_container.ts index 5d92f9de299..c39a9bc2a8d 100644 --- a/tensorboard/webapp/core/views/page_title_container.ts +++ b/tensorboard/webapp/core/views/page_title_container.ts @@ -18,6 +18,7 @@ import { Inject, Optional, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import { combineLatestWith, @@ -45,9 +46,7 @@ const DEFAULT_BRAND_NAME = 'TensorBoard'; @Component({ standalone: false, selector: 'page-title', - template: ` - - `, + template: ` `, styles: [ ` :host { @@ -62,7 +61,7 @@ export class PageTitleContainer { private readonly experimentName$; - readonly title$; + readonly title; constructor( private readonly store: Store, @@ -85,21 +84,27 @@ export class PageTitleContainer { }), map((experiment) => (experiment ? experiment.name : null)) ); - this.title$ = this.store.select(getEnvironment).pipe( - combineLatestWith(this.store.select(getRouteKind), this.experimentName$), - map(([env, routeKind, experimentName]) => { - const tbBrandName = this.customBrandName || DEFAULT_BRAND_NAME; - if (env.window_title) { - // (it's an empty string when the `--window_title` flag is not set) - return env.window_title; - } - if (routeKind === RouteKind.EXPERIMENT && experimentName) { - return `${experimentName} - ${tbBrandName}`; - } - return tbBrandName; - }), - startWith(this.customBrandName || DEFAULT_BRAND_NAME), - distinctUntilChanged() + this.title = toSignal( + this.store.select(getEnvironment).pipe( + combineLatestWith( + this.store.select(getRouteKind), + this.experimentName$ + ), + map(([env, routeKind, experimentName]) => { + const tbBrandName = this.customBrandName || DEFAULT_BRAND_NAME; + if (env.window_title) { + // (it's an empty string when the `--window_title` flag is not set) + return env.window_title; + } + if (routeKind === RouteKind.EXPERIMENT && experimentName) { + return `${experimentName} - ${tbBrandName}`; + } + return tbBrandName; + }), + startWith(this.customBrandName || DEFAULT_BRAND_NAME), + distinctUntilChanged() + ), + {requireSync: true} ); } } diff --git a/tensorboard/webapp/feature_flag/views/feature_flag_dialog_container.ts b/tensorboard/webapp/feature_flag/views/feature_flag_dialog_container.ts index b29cb822d72..407d555a79f 100644 --- a/tensorboard/webapp/feature_flag/views/feature_flag_dialog_container.ts +++ b/tensorboard/webapp/feature_flag/views/feature_flag_dialog_container.ts @@ -12,9 +12,9 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map, withLatestFrom} from 'rxjs/operators'; import {State} from '../../app_state'; import { @@ -44,75 +44,80 @@ import { standalone: false, selector: 'feature-flag-dialog', template: ``, }) export class FeatureFlagDialogContainer { constructor(private readonly store: Store) { - this.showFlagsFilter$ = this.store.select(getOverriddenFeatureFlags).pipe( + const showFlagsFilter$ = this.store.select(getOverriddenFeatureFlags).pipe( map((overriddenFeatureFlags) => { return overriddenFeatureFlags.showFlags?.toLowerCase(); }) ); - this.hasFlagsSentToServer$ = this.store - .select(getFeatureFlagsMetadata) - .pipe( + this.showFlagsFilter = toSignal(showFlagsFilter$, {requireSync: true}); + this.hasFlagsSentToServer = toSignal( + this.store.select(getFeatureFlagsMetadata).pipe( map((flagMetadata) => { return Object.values(flagMetadata).some((metadata) => { return (metadata as AdvancedFeatureFlagMetadata) .sendToServerWhenOverridden; }); }) - ); - this.featureFlags$ = this.store.select(getOverriddenFeatureFlags).pipe( - withLatestFrom( - this.store.select(getDefaultFeatureFlags), - this.store.select(getFeatureFlagsMetadata), - this.showFlagsFilter$ ), - map( - ([ - overriddenFeatureFlags, - defaultFeatureFlags, - flagMetadata, - showFlagsFilter, - ]) => { - return Object.entries(defaultFeatureFlags) - .filter(([flagName]) => { - if (!showFlagsFilter) { - return true; - } - return flagName.toLowerCase().includes(showFlagsFilter); - }) - .map(([flagName, defaultValue]) => { - const status = getFlagStatus( - flagName as keyof FeatureFlags, - overriddenFeatureFlags - ); - const metadata = flagMetadata[flagName as keyof FeatureFlags]; - return { - flag: flagName, - defaultValue, - status, - sendToServerWhenOverridden: ( - metadata as AdvancedFeatureFlagMetadata - ).sendToServerWhenOverridden, - } as FeatureFlagStatus; - }); - } - ) + {requireSync: true} + ); + this.featureFlags = toSignal( + this.store.select(getOverriddenFeatureFlags).pipe( + withLatestFrom( + this.store.select(getDefaultFeatureFlags), + this.store.select(getFeatureFlagsMetadata), + showFlagsFilter$ + ), + map( + ([ + overriddenFeatureFlags, + defaultFeatureFlags, + flagMetadata, + showFlagsFilter, + ]) => { + return Object.entries(defaultFeatureFlags) + .filter(([flagName]) => { + if (!showFlagsFilter) { + return true; + } + return flagName.toLowerCase().includes(showFlagsFilter); + }) + .map(([flagName, defaultValue]) => { + const status = getFlagStatus( + flagName as keyof FeatureFlags, + overriddenFeatureFlags + ); + const metadata = flagMetadata[flagName as keyof FeatureFlags]; + return { + flag: flagName, + defaultValue, + status, + sendToServerWhenOverridden: ( + metadata as AdvancedFeatureFlagMetadata + ).sendToServerWhenOverridden, + } as FeatureFlagStatus; + }); + } + ) + ), + {requireSync: true} ); } - readonly showFlagsFilter$; + readonly showFlagsFilter: Signal; - readonly hasFlagsSentToServer$: Observable; + readonly hasFlagsSentToServer: Signal; - readonly featureFlags$: Observable[]>; + readonly featureFlags: Signal[]>; onFlagChanged({flag, status}: FeatureFlagStatusEvent) { switch (status) { diff --git a/tensorboard/webapp/header/dark_mode_toggle_container.ts b/tensorboard/webapp/header/dark_mode_toggle_container.ts index 52a1362fddc..5bfea85228c 100644 --- a/tensorboard/webapp/header/dark_mode_toggle_container.ts +++ b/tensorboard/webapp/header/dark_mode_toggle_container.ts @@ -13,8 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {State as CoreState} from '../core/store/core_types'; import {overrideEnableDarkModeChanged} from '../feature_flag/actions/feature_flag_actions'; @@ -28,23 +28,26 @@ import {DarkModeOverride} from './dark_mode_toggle_component'; selector: 'app-header-dark-mode-toggle', template: ` `, }) export class DarkModeToggleContainer { - readonly darkModeOverride$: Observable; + readonly darkModeOverride; constructor(private readonly store: Store) { - this.darkModeOverride$ = this.store.select(getEnableDarkModeOverride).pipe( - map((override: boolean | null): DarkModeOverride => { - if (override === null) return DarkModeOverride.DEFAULT; - return override - ? DarkModeOverride.DARK_MODE_ON - : DarkModeOverride.DARK_MODE_OFF; - }) + this.darkModeOverride = toSignal( + this.store.select(getEnableDarkModeOverride).pipe( + map((override: boolean | null): DarkModeOverride => { + if (override === null) return DarkModeOverride.DEFAULT; + return override + ? DarkModeOverride.DARK_MODE_ON + : DarkModeOverride.DARK_MODE_OFF; + }) + ), + {requireSync: true} ); } diff --git a/tensorboard/webapp/header/plugin_selector_component.ts b/tensorboard/webapp/header/plugin_selector_component.ts index 4680736a1e9..095d9eae83d 100644 --- a/tensorboard/webapp/header/plugin_selector_component.ts +++ b/tensorboard/webapp/header/plugin_selector_component.ts @@ -38,7 +38,7 @@ export class PluginSelectorComponent { disabledPlugins!: UiPluginMetadata[]; @Input() - selectedPlugin!: PluginId; + selectedPlugin!: PluginId | null; @Output() onPluginSelectionChanged = new EventEmitter(); diff --git a/tensorboard/webapp/header/plugin_selector_container.ts b/tensorboard/webapp/header/plugin_selector_container.ts index a678c6ec63e..f8f1a8bc6ba 100644 --- a/tensorboard/webapp/header/plugin_selector_container.ts +++ b/tensorboard/webapp/header/plugin_selector_container.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {createSelector, select, Store} from '@ngrx/store'; +import {createSelector, Store} from '@ngrx/store'; import {changePlugin} from '../core/actions'; import {getActivePlugin, getPlugins, State} from '../core/store'; import {PluginId} from '../types/api'; @@ -34,22 +34,22 @@ const getDisabledPlugins = createSelector( selector: 'plugin-selector', template: ` `, }) export class PluginSelectorContainer { - readonly activePlugin$; - readonly plugins$; - readonly disabledPlugins$; + readonly activePlugin; + readonly plugins; + readonly disabledPlugins; constructor(private readonly store: Store) { - this.activePlugin$ = this.store.pipe(select(getActivePlugin)); - this.plugins$ = this.store.pipe(select(getUiPlugins)); - this.disabledPlugins$ = this.store.pipe(select(getDisabledPlugins)); + this.activePlugin = this.store.selectSignal(getActivePlugin); + this.plugins = this.store.selectSignal(getUiPlugins); + this.disabledPlugins = this.store.selectSignal(getDisabledPlugins); } onPluginSelectionChange(pluginId: PluginId) { diff --git a/tensorboard/webapp/header/reload_container.ts b/tensorboard/webapp/header/reload_container.ts index d8a6d1078f4..ee7ff80be5e 100644 --- a/tensorboard/webapp/header/reload_container.ts +++ b/tensorboard/webapp/header/reload_container.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {createSelector, Store} from '@ngrx/store'; import {Observable} from 'rxjs'; import {combineLatestWith, map} from 'rxjs/operators'; @@ -42,11 +43,11 @@ const isReloadDisabledByPlugin = createSelector( template: ` @@ -79,19 +80,22 @@ const isReloadDisabledByPlugin = createSelector( ], }) export class ReloadContainer { - readonly reloadDisabled$: Observable; - - isReloading$: Observable; + readonly reloadDisabled; + readonly isReloading; lastLoadedTimeInMs$: Observable; constructor(private readonly store: Store) { - this.reloadDisabled$ = this.store.select(isReloadDisabledByPlugin); - this.isReloading$ = this.store.select(getCoreDataLoadedState).pipe( - combineLatestWith(this.reloadDisabled$), - map(([loadState, reloadDisabled]) => { - return !reloadDisabled && loadState === DataLoadState.LOADING; - }) + const reloadDisabled$ = this.store.select(isReloadDisabledByPlugin); + this.reloadDisabled = toSignal(reloadDisabled$, {requireSync: true}); + this.isReloading = toSignal( + this.store.select(getCoreDataLoadedState).pipe( + combineLatestWith(reloadDisabled$), + map(([loadState, reloadDisabled]) => { + return !reloadDisabled && loadState === DataLoadState.LOADING; + }) + ), + {requireSync: true} ); this.lastLoadedTimeInMs$ = this.store.select(getAppLastLoadedTimeInMs); } diff --git a/tensorboard/webapp/metrics/views/card_renderer/card_view_container.ts b/tensorboard/webapp/metrics/views/card_renderer/card_view_container.ts index 22d1f775084..8da3fcd89f1 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/card_view_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/card_view_container.ts @@ -18,9 +18,10 @@ import { EventEmitter, Input, Output, + Signal, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map, take, throttleTime, withLatestFrom} from 'rxjs/operators'; import {State} from '../../../app_state'; import * as selectors from '../../../selectors'; @@ -44,7 +45,7 @@ const RUN_COLOR_UPDATE_THROTTLE_TIME_IN_MS = 350; [cardId]="cardId" [groupName]="groupName" [pluginType]="pluginType" - [runColorScale]="runColorScale$ | async" + [runColorScale]="runColorScale()" (fullWidthChanged)="onFullWidthChanged($event)" (fullHeightChanged)="onFullHeightChanged($event)" (pinStateChanged)="onPinStateChanged()" @@ -59,21 +60,24 @@ const RUN_COLOR_UPDATE_THROTTLE_TIME_IN_MS = 350; }) export class CardViewContainer { constructor(private readonly store: Store) { - this.runColorScale$ = this.store.select(selectors.getRunColorMap).pipe( - throttleTime(RUN_COLOR_UPDATE_THROTTLE_TIME_IN_MS, undefined, { - leading: true, - trailing: true, - }), - map((colorMap) => { - return (runId: string) => { - if (!colorMap.hasOwnProperty(runId)) { - // Assign white when no colors are assigned to a run by user or - // by color grouping scheme. - return '#fff'; - } - return colorMap[runId]; - }; - }) + this.runColorScale = toSignal( + this.store.select(selectors.getRunColorMap).pipe( + throttleTime(RUN_COLOR_UPDATE_THROTTLE_TIME_IN_MS, undefined, { + leading: true, + trailing: true, + }), + map((colorMap) => { + return (runId: string) => { + if (!colorMap.hasOwnProperty(runId)) { + // Assign white when no colors are assigned to a run by user or + // by color grouping scheme. + return '#fff'; + } + return colorMap[runId]; + }; + }) + ), + {requireSync: true} ); } @@ -90,7 +94,7 @@ export class CardViewContainer { this.isEverVisible = this.isEverVisible || visible; } - readonly runColorScale$: Observable; + readonly runColorScale: Signal; onFullWidthChanged(showFullWidth: boolean) { this.fullWidthChanged.emit(showFullWidth); diff --git a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ng.html index af9cc394066..e6da834b065 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ng.html @@ -55,7 +55,7 @@ [disabled]="!downloadUrlJson" [download]="getDownloadName('json')" [href]="downloadUrlJson" - [includeFeatureFlags] + [includeFeatureFlags]="true" >JSON [disabled]="!downloadUrlCsv" [download]="getDownloadName('csv')" [href]="downloadUrlCsv" - [includeFeatureFlags] + [includeFeatureFlags]="true" >CSV diff --git a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ts b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ts index adb1e711817..87ffcaa10b8 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_component.ts @@ -32,7 +32,7 @@ import {CardMetadata} from '../../types'; }) export class DataDownloadDialogComponent { @Input() - cardMetadata!: CardMetadata; + cardMetadata: CardMetadata | null = null; @Input() runs!: Run[]; diff --git a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_container.ts b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_container.ts index 9da802105fa..59ec1c88a8f 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_container.ts @@ -12,7 +12,13 @@ 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 {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Signal, +} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import {Store} from '@ngrx/store'; import {BehaviorSubject, combineLatest, Observable} from 'rxjs'; @@ -36,21 +42,22 @@ export interface DataDownloadDialogData { selector: 'data_download_dialog', template: ``, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataDownloadDialogContainer { - readonly runs$: Observable; + readonly runs: Signal; readonly cardMetadata$: Observable; readonly selectedRunId$ = new BehaviorSubject(null); - readonly downloadUrlCsv$: Observable; - readonly downloadUrlJson$: Observable; + readonly selectedRunId: Signal; + readonly downloadUrlCsv: Signal; + readonly downloadUrlJson: Signal; constructor( store: Store, @@ -63,49 +70,60 @@ export class DataDownloadDialogContainer { filter((metadata) => Boolean(metadata)) ) as Observable; - this.downloadUrlCsv$ = combineLatest([ - store.select(getCardMetadata, data.cardId), - this.selectedRunId$, - ]).pipe( - map(([metadata, selectedRunId]): string | null => { - if (!metadata || !selectedRunId) return null; - return dataSource.downloadUrl( - metadata.plugin, - metadata.tag, - selectedRunId!, - 'csv' - ); - }), - startWith(null) + this.selectedRunId = toSignal(this.selectedRunId$, {requireSync: true}); + + this.downloadUrlCsv = toSignal( + combineLatest([ + store.select(getCardMetadata, data.cardId), + this.selectedRunId$, + ]).pipe( + map(([metadata, selectedRunId]): string | null => { + if (!metadata || !selectedRunId) return null; + return dataSource.downloadUrl( + metadata.plugin, + metadata.tag, + selectedRunId!, + 'csv' + ); + }), + startWith(null) + ), + {requireSync: true} ); - this.downloadUrlJson$ = combineLatest([ - store.select(getCardMetadata, data.cardId), - this.selectedRunId$, - ]).pipe( - map(([metadata, selectedRunId]): string | null => { - if (!metadata || !selectedRunId) return null; - return dataSource.downloadUrl( - metadata.plugin, - metadata.tag, - selectedRunId!, - 'json' - ); - }), - startWith(null) + this.downloadUrlJson = toSignal( + combineLatest([ + store.select(getCardMetadata, data.cardId), + this.selectedRunId$, + ]).pipe( + map(([metadata, selectedRunId]): string | null => { + if (!metadata || !selectedRunId) return null; + return dataSource.downloadUrl( + metadata.plugin, + metadata.tag, + selectedRunId!, + 'json' + ); + }), + startWith(null) + ), + {requireSync: true} ); - this.runs$ = combineLatest([ - store.select(getRunMap), - store.select(getCardTimeSeries, data.cardId), - ]).pipe( - map(([runMap, runToSeries]) => { - if (!runToSeries) return []; - return Object.keys(runToSeries) - .map((runId) => { - return runMap.get(runId); - }) - .filter(Boolean) as Run[]; - }) + this.runs = toSignal( + combineLatest([ + store.select(getRunMap), + store.select(getCardTimeSeries, data.cardId), + ]).pipe( + map(([runMap, runToSeries]) => { + if (!runToSeries) return []; + return Object.keys(runToSeries) + .map((runId) => { + return runMap.get(runId); + }) + .filter(Boolean) as Run[]; + }) + ), + {requireSync: true} ); } } diff --git a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_test.ts b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_test.ts index 0a74d72f36c..8f29bc4cf89 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/data_download_dialog_test.ts @@ -105,6 +105,7 @@ describe('metrics/views/data_download_dialog', () => { ], ]) ); + store.refreshState(); fixture.detectChanges(); const options = fixture.debugElement.queryAll(ByCss.SELECT_OPTION); @@ -143,6 +144,7 @@ describe('metrics/views/data_download_dialog', () => { ], ]) ); + store.refreshState(); fixture.detectChanges(); const options = fixture.debugElement.queryAll(ByCss.SELECT_OPTION); @@ -189,6 +191,7 @@ describe('metrics/views/data_download_dialog', () => { ], ]) ); + store.refreshState(); fixture.detectChanges(); const selectEl = fixture.debugElement.query(ByCss.SELECT).nativeElement; @@ -244,6 +247,7 @@ describe('metrics/views/data_download_dialog', () => { ], ]) ); + store.refreshState(); fixture.detectChanges(); const selectEl = fixture.debugElement.query(ByCss.SELECT).nativeElement; @@ -300,6 +304,7 @@ describe('metrics/views/data_download_dialog', () => { ], ]) ); + store.refreshState(); fixture.detectChanges(); const selectEl = fixture.debugElement.query(ByCss.SELECT).nativeElement; diff --git a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_component.ng.html index e3dc2f57ed0..333cef2645e 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_component.ng.html @@ -16,13 +16,16 @@ --> - + - + ) { - this.mode$ = this.store.select(getMetricsHistogramMode); - this.xAxisType$ = this.store.select(getMetricsXAxisType); - this.showFullWidth$ = this.store - .select(getCardStateMap) - .pipe(map((map) => map[this.cardId]?.fullWidth)); + constructor( + private readonly store: Store, + private readonly injector: Injector + ) { + this.mode = this.store.selectSignal(getMetricsHistogramMode); + this.xAxisType = this.store.selectSignal(getMetricsXAxisType); + this.showFullWidth = toSignal( + this.store + .select(getCardStateMap) + .pipe(map((map) => map[this.cardId]?.fullWidth ?? false)), + {requireSync: true} + ); } @Input() cardId!: CardId; @@ -112,15 +121,15 @@ export class HistogramCardContainer implements CardRenderer, OnInit { @Input() runColorScale!: RunColorScale; @Output() pinStateChanged = new EventEmitter(); - loadState$?: Observable; + loadState!: Signal; title$?: Observable; tag$?: Observable; runId$?: Observable; data$?: Observable; - mode$; - xAxisType$; - readonly showFullWidth$; - isPinned$?: Observable; + readonly mode; + readonly xAxisType; + readonly showFullWidth; + isPinned!: Signal; linkedTimeSelection$?: Observable; isClosestStepHighlighted$?: Observable; isTimeSelectionClipped$?: Observable; @@ -220,7 +229,10 @@ export class HistogramCardContainer implements CardRenderer, OnInit { }) ); - this.loadState$ = this.store.select(getCardLoadState, this.cardId); + this.loadState = toSignal( + this.store.select(getCardLoadState, this.cardId), + {injector: this.injector, requireSync: true} + ); this.tag$ = cardMetadata$.pipe( map((cardMetadata) => { @@ -240,7 +252,10 @@ export class HistogramCardContainer implements CardRenderer, OnInit { }) ); - this.isPinned$ = this.store.select(getCardPinnedState, this.cardId); + this.isPinned = toSignal( + this.store.select(getCardPinnedState, this.cardId), + {injector: this.injector, requireSync: true} + ); } onLinkedTimeSelectionChanged( diff --git a/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ng.html index 8da2464812d..0cca8309fdc 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ng.html @@ -23,7 +23,7 @@ value="{{ title }}" > @@ -52,7 +52,7 @@ - + Step {{ steps[stepIndex] | number }} - 1" + 1" >Sample {{ sample + 1 | number }}/{{ numSample | number}} diff --git a/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ts b/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ts index 86a852be5e2..de6ce9f993b 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/image_card_component.ts @@ -43,11 +43,11 @@ export class ImageCardComponent { sliderTrackWidth = ''; @Input() loadState!: DataLoadState; - @Input() title!: string; - @Input() tag!: string; - @Input() runId!: string; - @Input() sample!: number; - @Input() numSample!: number; + @Input() title: string | null = null; + @Input() tag: string | null = null; + @Input() runId: string | null = null; + @Input() sample: number | null = null; + @Input() numSample: number | null = null; @Input() imageUrl!: string | null; @Input() stepIndex!: number | null; @Input() steps!: number[]; @@ -57,7 +57,7 @@ export class ImageCardComponent { @Input() runColorScale!: RunColorScale; @Input() allowToggleActualSize!: boolean; @Input() isPinned!: boolean; - @Input() selectedSteps!: number[]; + @Input() selectedSteps: number[] | null = null; @Input() linkedTimeSelection?: TimeSelectionView | null = null; @Input() isClosestStepHighlighted?: boolean = false; diff --git a/tensorboard/webapp/metrics/views/card_renderer/image_card_container.ts b/tensorboard/webapp/metrics/views/card_renderer/image_card_container.ts index 984306f18a8..aaba901fd27 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/image_card_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/image_card_container.ts @@ -16,11 +16,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, + Injector, Input, OnDestroy, OnInit, Output, + Signal, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import { @@ -72,7 +75,7 @@ type ImageCardMetadata = CardMetadata & { selector: 'image-card', template: ` , - private readonly dataSource: MetricsDataSource + private readonly dataSource: MetricsDataSource, + private readonly injector: Injector ) { - this.brightnessInMilli$ = this.store.select( + this.brightnessInMilli = this.store.selectSignal( getMetricsImageBrightnessInMilli ); - this.contrastInMilli$ = this.store.select(getMetricsImageContrastInMilli); + this.contrastInMilli = this.store.selectSignal( + getMetricsImageContrastInMilli + ); this.actualSizeGlobalSetting$ = this.store.select( getMetricsImageShowActualSize ); @@ -135,7 +141,7 @@ export class ImageCardContainer implements CardRenderer, OnInit, OnDestroy { ); } - loadState$?: Observable; + loadState!: Signal; title$?: Observable; tag$?: Observable; runId$?: Observable; @@ -143,13 +149,13 @@ export class ImageCardContainer implements CardRenderer, OnInit, OnDestroy { numSample$?: Observable; imageUrl$?: Observable; stepIndex$?: Observable; - isClosestStepHighlighted$?: Observable; - steps$?: Observable; - isPinned$?: Observable; + isClosestStepHighlighted!: Signal; + steps!: Signal; + isPinned!: Signal; linkedTimeSelection$?: Observable; selectedSteps$?: Observable; - brightnessInMilli$; - contrastInMilli$; + readonly brightnessInMilli; + readonly contrastInMilli; actualSizeGlobalSetting$; showActualSize = false; @@ -231,14 +237,20 @@ export class ImageCardContainer implements CardRenderer, OnInit, OnDestroy { stepIndexMetaData ? stepIndexMetaData.index : null ) ); - this.isClosestStepHighlighted$ = this.store - .select(getCardStepIndexMetaData, this.cardId) - .pipe( - map((stepIndexMetaData) => - stepIndexMetaData ? stepIndexMetaData.isClosest : false - ) - ); - this.loadState$ = this.store.select(getCardLoadState, this.cardId); + this.isClosestStepHighlighted = toSignal( + this.store + .select(getCardStepIndexMetaData, this.cardId) + .pipe( + map((stepIndexMetaData) => + stepIndexMetaData ? stepIndexMetaData.isClosest ?? false : false + ) + ), + {injector: this.injector, requireSync: true} + ); + this.loadState = toSignal( + this.store.select(getCardLoadState, this.cardId), + {injector: this.injector, requireSync: true} + ); this.tag$ = cardMetadata$.pipe( map((cardMetadata) => { @@ -268,14 +280,21 @@ export class ImageCardContainer implements CardRenderer, OnInit, OnDestroy { map((cardMetadata) => cardMetadata.numSample) ); - this.steps$ = this.store.select(getMetricsImageCardSteps, this.cardId); + const steps$ = this.store.select(getMetricsImageCardSteps, this.cardId); + this.steps = toSignal(steps$, { + injector: this.injector, + requireSync: true, + }); - this.isPinned$ = this.store.select(getCardPinnedState, this.cardId); + this.isPinned = toSignal( + this.store.select(getCardPinnedState, this.cardId), + {injector: this.injector, requireSync: true} + ); this.linkedTimeSelection$ = this.store .select(getMetricsLinkedTimeSelection) .pipe( - combineLatestWith(this.steps$), + combineLatestWith(steps$), map(([linkedTimeSelection, steps]) => { if (!linkedTimeSelection) return null; @@ -291,7 +310,7 @@ export class ImageCardContainer implements CardRenderer, OnInit, OnDestroy { // TODO(japie1235813): Reuses `getSelectedSteps` in store_utils. this.selectedSteps$ = this.linkedTimeSelection$.pipe( - combineLatestWith(this.steps$), + combineLatestWith(steps$), map(([linkedTimeSelection, steps]) => { if (!linkedTimeSelection) return []; diff --git a/tensorboard/webapp/metrics/views/card_renderer/run_name_container.ts b/tensorboard/webapp/metrics/views/card_renderer/run_name_container.ts index 156a96ddd85..ea8a8f508b2 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/run_name_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/run_name_container.ts @@ -12,9 +12,17 @@ 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 {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Injector, + Input, + OnInit, + Signal, +} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {combineLatest, Observable} from 'rxjs'; +import {combineLatest} from 'rxjs'; import {map} from 'rxjs/operators'; import {State} from '../../../app_state'; import {ExperimentAlias} from '../../../experiments/types'; @@ -30,9 +38,9 @@ import {getDisplayNameForRun} from './utils'; selector: 'card-run-name', template: ` `, changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,29 +48,40 @@ import {getDisplayNameForRun} from './utils'; export class RunNameContainer implements OnInit { @Input() runId!: string; - name$?: Observable; - experimentAlias$?: Observable; + name!: Signal; + experimentAlias!: Signal; - constructor(private readonly store: Store) {} + constructor( + private readonly store: Store, + private readonly injector: Injector + ) {} /** * Build observables once runId is defined (after onInit). */ ngOnInit() { - this.name$ = combineLatest([ - this.store.select(getRun, {runId: this.runId}), - ]).pipe( - map(([run]) => { - return getDisplayNameForRun(this.runId, run, /*experimentAlias=*/ null); - }) + this.name = toSignal( + combineLatest([this.store.select(getRun, {runId: this.runId})]).pipe( + map(([run]) => { + return getDisplayNameForRun( + this.runId, + run, + /*experimentAlias=*/ null + ); + }) + ), + {injector: this.injector, requireSync: true} ); - this.experimentAlias$ = combineLatest([ - this.store.select(getExperimentIdForRunId, {runId: this.runId}), - this.store.select(getExperimentIdToExperimentAliasMap), - ]).pipe( - map(([experimentId, idToAlias]) => { - return experimentId ? idToAlias[experimentId] : null; - }) + this.experimentAlias = toSignal( + combineLatest([ + this.store.select(getExperimentIdForRunId, {runId: this.runId}), + this.store.select(getExperimentIdToExperimentAliasMap), + ]).pipe( + map(([experimentId, idToAlias]) => { + return experimentId ? idToAlias[experimentId] : null; + }) + ), + {injector: this.injector, requireSync: true} ); } } diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html index ed35ca80e2b..a2ecb18160f 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html @@ -23,7 +23,7 @@ value="{{ title }}" > @@ -287,7 +287,7 @@ [timeSelection]="stepOrLinkedTimeSelection" [scale]="xScale" [minMaxHorizontalViewExtend]="viewExtent.x" - [minMaxStep]="minMaxStep" + [minMaxStep]="minMaxStep!" [axisSize]="domDim.width" (onTimeSelectionChanged)="onTimeSelectionChanged.emit($event)" (onTimeSelectionToggled)="onFobRemoved()" diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts index 8cfd926a687..e12b5c0babb 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts @@ -102,8 +102,8 @@ export class ScalarCardComponent { @Input() loadState!: DataLoadState; @Input() showFullWidth!: boolean; @Input() smoothingEnabled!: boolean; - @Input() tag!: string; - @Input() title!: string; + @Input() tag: string | null = null; + @Input() title: string | null = null; @Input() tooltipSort!: TooltipSort; @Input() xAxisType!: XAxisType; @Input() xScaleType!: ScaleType; @@ -111,9 +111,9 @@ export class ScalarCardComponent { @Input() forceSvg!: boolean; @Input() columnCustomizationEnabled!: boolean; @Input() columnContextMenusEnabled!: boolean; - @Input() linkedTimeSelection: TimeSelectionView | undefined; + @Input() linkedTimeSelection: TimeSelectionView | undefined | null; @Input() stepOrLinkedTimeSelection: TimeSelection | undefined; - @Input() minMaxStep!: MinMaxStep; + @Input() minMaxStep: MinMaxStep | undefined | null; @Input() userViewBox!: Extent | null; @Input() columnHeaders!: ColumnHeader[]; @Input() rangeEnabled!: boolean; diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts index 931143d63ae..e2fe1055c28 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts @@ -17,11 +17,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, + Injector, Input, OnDestroy, OnInit, Output, + Signal, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {combineLatest, from, Observable, of, Subject} from 'rxjs'; import { @@ -170,38 +173,38 @@ function areSeriesEqual( template: ` ) { - this.columnFilters$ = this.store.select(getCurrentColumnFilters); - this.numColumnsLoaded$ = this.store.select( + constructor( + private readonly store: Store, + private readonly injector: Injector + ) { + this.columnFilters = this.store.selectSignal(getCurrentColumnFilters); + this.numColumnsLoaded = this.store.selectSignal( hparamsSelectors.getNumDashboardHparamsLoaded ); - this.numColumnsToLoad$ = this.store.select( + this.numColumnsToLoad = this.store.selectSignal( hparamsSelectors.getNumDashboardHparamsToLoad ); - this.useDarkMode$ = this.store.select(getDarkModeEnabled); - this.ignoreOutliers$ = this.store.select(getMetricsIgnoreOutliers); - this.isTooltipRowsLimitEnabled$ = this.store.select( + this.useDarkMode = this.store.selectSignal(getDarkModeEnabled); + this.ignoreOutliers = this.store.selectSignal(getMetricsIgnoreOutliers); + this.isTooltipRowsLimitEnabled = this.store.selectSignal( getMetricsIsTooltipRowsLimitEnabled ); - this.tooltipRowsLimit$ = this.store.select(getMetricsTooltipRowsLimit); - this.tooltipSort$ = this.store.select(getMetricsTooltipSort); - this.xAxisType$ = this.store.select(getMetricsXAxisType); - this.forceSvg$ = this.store.select(getForceSvgFeatureFlag); - this.columnCustomizationEnabled$ = this.store.select( + this.tooltipRowsLimit = this.store.selectSignal(getMetricsTooltipRowsLimit); + this.tooltipSort = this.store.selectSignal(getMetricsTooltipSort); + this.xAxisType = this.store.selectSignal(getMetricsXAxisType); + this.forceSvg = this.store.selectSignal(getForceSvgFeatureFlag); + this.columnCustomizationEnabled = this.store.selectSignal( getIsScalarColumnCustomizationEnabled ); - this.columnContextMenusEnabled$ = this.store.select( + this.columnContextMenusEnabled = this.store.selectSignal( getIsScalarColumnContextMenusEnabled ); - this.xScaleType$ = this.store.select(getMetricsXAxisType).pipe( - map((xAxisType) => { - switch (xAxisType) { - case XAxisType.STEP: - case XAxisType.RELATIVE: - return ScaleType.LINEAR; - case XAxisType.WALL_TIME: - return ScaleType.TIME; - default: - const neverType = xAxisType as never; - throw new Error(`Invalid xAxisType for line chart. ${neverType}`); - } - }) + this.xScaleType = toSignal( + this.store.select(getMetricsXAxisType).pipe( + map((xAxisType) => { + switch (xAxisType) { + case XAxisType.STEP: + case XAxisType.RELATIVE: + return ScaleType.LINEAR; + case XAxisType.WALL_TIME: + return ScaleType.TIME; + default: + const neverType = xAxisType as never; + throw new Error(`Invalid xAxisType for line chart. ${neverType}`); + } + }) + ), + {requireSync: true} ); this.scalarSmoothing$ = this.store.select(getMetricsScalarSmoothing); - this.smoothingEnabled$ = this.store - .select(getMetricsScalarSmoothing) - .pipe(map((smoothing) => smoothing > 0)); - this.showFullWidth$ = this.store - .select(getCardStateMap) - .pipe(map((map) => map[this.cardId]?.fullWidth)); + this.smoothingEnabled = toSignal( + this.store + .select(getMetricsScalarSmoothing) + .pipe(map((smoothing) => smoothing > 0)), + {requireSync: true} + ); + this.showFullWidth = toSignal( + this.store + .select(getCardStateMap) + .pipe(map((map) => map[this.cardId]?.fullWidth ?? false)), + {requireSync: true} + ); } // Angular Component constructor for DataDownload dialog. It is customizable for @@ -286,45 +301,45 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { @Output() pinStateChanged = new EventEmitter(); isVisible: boolean = false; - loadState$?: Observable; + loadState!: Signal; title$?: Observable; tag$?: Observable; - isPinned$?: Observable; - dataSeries$?: Observable; - chartMetadataMap$?: Observable; + isPinned!: Signal; + dataSeries!: Signal; + chartMetadataMap!: Signal; linkedTimeSelection$?: Observable; - columnHeaders$?: Observable; + columnHeaders!: Signal; minMaxSteps$?: Observable; - userViewBox$?: Observable; - stepOrLinkedTimeSelection$?: Observable; - cardState$?: Observable>; - rangeEnabled$?: Observable; + userViewBox!: Signal; + stepOrLinkedTimeSelection!: Signal; + cardState!: Signal>; + rangeEnabled!: Signal; hparamsEnabled$?: Observable; - columnFilters$; - runToHparamMap$?: Observable; - selectableColumns$?: Observable; - numColumnsLoaded$; - numColumnsToLoad$; + readonly columnFilters; + runToHparamMap!: Signal; + selectableColumns!: Signal; + readonly numColumnsLoaded; + readonly numColumnsToLoad; onVisibilityChange({visible}: {visible: boolean}) { this.isVisible = visible; } - readonly useDarkMode$; - readonly ignoreOutliers$; - readonly isTooltipRowsLimitEnabled$; - readonly tooltipRowsLimit$; - readonly tooltipSort$; - readonly xAxisType$; - readonly forceSvg$; - readonly columnCustomizationEnabled$; - readonly columnContextMenusEnabled$; - readonly xScaleType$; + readonly useDarkMode; + readonly ignoreOutliers; + readonly isTooltipRowsLimitEnabled; + readonly tooltipRowsLimit; + readonly tooltipSort; + readonly xAxisType; + readonly forceSvg; + readonly columnCustomizationEnabled; + readonly columnContextMenusEnabled; + readonly xScaleType; readonly scalarSmoothing$; - readonly smoothingEnabled$; + readonly smoothingEnabled; - readonly showFullWidth$; + readonly showFullWidth; private readonly ngUnsubscribe = new Subject(); @@ -440,9 +455,9 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { shareReplay(1) ); - this.userViewBox$ = this.store.select( - getMetricsCardUserViewBox, - this.cardId + this.userViewBox = toSignal( + this.store.select(getMetricsCardUserViewBox, this.cardId), + {injector: this.injector, requireSync: true} ); this.minMaxSteps$ = combineLatest([ @@ -460,38 +475,41 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { }) ); - this.dataSeries$ = partitionedSeries$.pipe( - // Smooth - combineLatestWith(this.store.select(getMetricsScalarSmoothing)), - switchMap< - [PartitionedSeries[], number], - Observable - >(([runsData, smoothing]) => { - const cleanedRunsData = runsData.map(({seriesId, points}) => ({ - id: seriesId, - points, - })); - if (smoothing <= 0) { - return of(cleanedRunsData); - } + this.dataSeries = toSignal( + partitionedSeries$.pipe( + // Smooth + combineLatestWith(this.store.select(getMetricsScalarSmoothing)), + switchMap< + [PartitionedSeries[], number], + Observable + >(([runsData, smoothing]) => { + const cleanedRunsData = runsData.map(({seriesId, points}) => ({ + id: seriesId, + points, + })); + if (smoothing <= 0) { + return of(cleanedRunsData); + } - return from(classicSmoothing(cleanedRunsData, smoothing)).pipe( - map((smoothedDataSeriesList) => { - const smoothedList = cleanedRunsData.map((dataSeries, index) => { - return { - id: getSmoothedSeriesId(dataSeries.id), - points: smoothedDataSeriesList[index].points.map( - ({y}, pointIndex) => { - return {...dataSeries.points[pointIndex], y}; - } - ), - }; - }); - return [...cleanedRunsData, ...smoothedList]; - }) - ); - }), - startWith([] as ScalarCardDataSeries[]) + return from(classicSmoothing(cleanedRunsData, smoothing)).pipe( + map((smoothedDataSeriesList) => { + const smoothedList = cleanedRunsData.map((dataSeries, index) => { + return { + id: getSmoothedSeriesId(dataSeries.id), + points: smoothedDataSeriesList[index].points.map( + ({y}, pointIndex) => { + return {...dataSeries.points[pointIndex], y}; + } + ), + }; + }); + return [...cleanedRunsData, ...smoothedList]; + }) + ); + }), + startWith([] as ScalarCardDataSeries[]) + ), + {injector: this.injector, requireSync: true} ); this.linkedTimeSelection$ = combineLatest([ @@ -518,113 +536,120 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { }) ); - this.stepOrLinkedTimeSelection$ = this.store.select( - getMetricsCardTimeSelection, - this.cardId + this.stepOrLinkedTimeSelection = toSignal( + this.store.select(getMetricsCardTimeSelection, this.cardId), + {injector: this.injector, requireSync: true} ); - this.columnHeaders$ = this.store.select( - getGroupedHeadersForCard(this.cardId) + this.columnHeaders = toSignal( + this.store.select(getGroupedHeadersForCard(this.cardId)), + {injector: this.injector, requireSync: true} ); - this.chartMetadataMap$ = partitionedSeries$.pipe( - switchMap< - PartitionedSeries[], - Observable< - Array< - PartitionedSeries & { - displayName: string; - alias: ExperimentAlias | null; - } + this.chartMetadataMap = toSignal( + partitionedSeries$.pipe( + switchMap< + PartitionedSeries[], + Observable< + Array< + PartitionedSeries & { + displayName: string; + alias: ExperimentAlias | null; + } + > > - > - >((partitioned) => { - return combineLatest( - partitioned.map((series) => { - return this.getRunDisplayNameAndAlias(series.runId).pipe( - map((displayNameAndAlias) => { - return {...series, ...displayNameAndAlias}; - }) - ); - }) - ); - }), - combineLatestWith( - this.store.select(getCurrentRouteRunSelection), - this.store.select(getFilteredRenderableRunsIds), - this.store.select(getRunColorMap), - this.store.select(getMetricsScalarSmoothing) - ), - // When the `fetchRunsSucceeded` action fires, the run selection - // map and the metadata change. To prevent quick fire of changes, - // debounce by a microtask to emit only single change for the runs - // store change. - debounceTime(0), - map( - ([ - namedPartitionedSeries, - runSelectionMap, - renderableRuns, - colorMap, - smoothing, - ]) => { - const metadataMap: ScalarCardSeriesMetadataMap = {}; - const shouldSmooth = smoothing > 0; - - for (const partitioned of namedPartitionedSeries) { - const { - seriesId, - runId, - displayName, - alias, - partitionIndex, - partitionSize, - } = partitioned; - - metadataMap[seriesId] = { - type: SeriesType.ORIGINAL, - id: seriesId, - alias, - displayName: - partitionSize > 1 - ? `${displayName}: ${partitionIndex}` - : displayName, - visible: Boolean( - runSelectionMap && - runSelectionMap.get(runId) && - renderableRuns.has(runId) - ), - color: colorMap[runId] ?? '#fff', - aux: false, - opacity: 1, - }; - } + >((partitioned) => { + return combineLatest( + partitioned.map((series) => { + return this.getRunDisplayNameAndAlias(series.runId).pipe( + map((displayNameAndAlias) => { + return {...series, ...displayNameAndAlias}; + }) + ); + }) + ); + }), + combineLatestWith( + this.store.select(getCurrentRouteRunSelection), + this.store.select(getFilteredRenderableRunsIds), + this.store.select(getRunColorMap), + this.store.select(getMetricsScalarSmoothing) + ), + // When the `fetchRunsSucceeded` action fires, the run selection + // map and the metadata change. To prevent quick fire of changes, + // debounce by a microtask to emit only single change for the runs + // store change. + debounceTime(0), + map( + ([ + namedPartitionedSeries, + runSelectionMap, + renderableRuns, + colorMap, + smoothing, + ]) => { + const metadataMap: ScalarCardSeriesMetadataMap = {}; + const shouldSmooth = smoothing > 0; + + for (const partitioned of namedPartitionedSeries) { + const { + seriesId, + runId, + displayName, + alias, + partitionIndex, + partitionSize, + } = partitioned; + + metadataMap[seriesId] = { + type: SeriesType.ORIGINAL, + id: seriesId, + alias, + displayName: + partitionSize > 1 + ? `${displayName}: ${partitionIndex}` + : displayName, + visible: Boolean( + runSelectionMap && + runSelectionMap.get(runId) && + renderableRuns.has(runId) + ), + color: colorMap[runId] ?? '#fff', + aux: false, + opacity: 1, + }; + } - if (!shouldSmooth) { - return metadataMap; - } + if (!shouldSmooth) { + return metadataMap; + } - for (const [id, metadata] of Object.entries(metadataMap)) { - const smoothedSeriesId = getSmoothedSeriesId(id); - metadataMap[smoothedSeriesId] = { - ...metadata, - id: smoothedSeriesId, - type: SeriesType.DERIVED, - aux: false, - originalSeriesId: id, - }; + for (const [id, metadata] of Object.entries(metadataMap)) { + const smoothedSeriesId = getSmoothedSeriesId(id); + metadataMap[smoothedSeriesId] = { + ...metadata, + id: smoothedSeriesId, + type: SeriesType.DERIVED, + aux: false, + originalSeriesId: id, + }; - metadata.aux = true; - metadata.opacity = 0.25; - } + metadata.aux = true; + metadata.opacity = 0.25; + } - return metadataMap; - } + return metadataMap; + } + ), + startWith({} as ScalarCardSeriesMetadataMap) ), - startWith({} as ScalarCardSeriesMetadataMap) + {injector: this.injector, requireSync: true} ); - this.loadState$ = this.store.select(getCardLoadState, this.cardId); + this.loadState = toSignal( + this.store.select(getCardLoadState, this.cardId), + {injector: this.injector, requireSync: true} + ); this.tag$ = cardMetadata$.pipe( map((cardMetadata) => { @@ -632,10 +657,13 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { }) ); - this.cardState$ = this.store.select(getCardStateMap).pipe( - map((cardStateMap) => { - return cardStateMap[this.cardId] || {}; - }) + this.cardState = toSignal( + this.store.select(getCardStateMap).pipe( + map((cardStateMap) => { + return cardStateMap[this.cardId] || {}; + }) + ), + {injector: this.injector, requireSync: true} ); this.title$ = this.tag$.pipe( @@ -644,15 +672,25 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { }) ); - this.isPinned$ = this.store.select(getCardPinnedState, this.cardId); + this.isPinned = toSignal( + this.store.select(getCardPinnedState, this.cardId), + {injector: this.injector, requireSync: true} + ); - this.rangeEnabled$ = this.store.select( - getMetricsCardRangeSelectionEnabled(this.cardId) + this.rangeEnabled = toSignal( + this.store.select(getMetricsCardRangeSelectionEnabled(this.cardId)), + {injector: this.injector, requireSync: true} ); - this.runToHparamMap$ = this.store.select(getRunToHparamMap); + this.runToHparamMap = toSignal(this.store.select(getRunToHparamMap), { + injector: this.injector, + requireSync: true, + }); - this.selectableColumns$ = this.store.select(getSelectableColumns); + this.selectableColumns = toSignal(this.store.select(getSelectableColumns), { + injector: this.injector, + requireSync: true, + }); } ngOnDestroy() { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_data_table.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_data_table.ts index 410be6cb6d9..19662d67e74 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_data_table.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_data_table.ts @@ -56,7 +56,7 @@ import {memoize} from '../../../util/memoize'; export class ScalarCardDataTable { @Input() chartMetadataMap!: ScalarCardSeriesMetadataMap; @Input() dataSeries!: ScalarCardDataSeries[]; - @Input() stepOrLinkedTimeSelection!: TimeSelection; + @Input() stepOrLinkedTimeSelection: TimeSelection | undefined; @Input() columnHeaders!: ColumnHeader[]; @Input() sortingInfo!: SortingInfo; @Input() columnCustomizationEnabled!: boolean; @@ -326,7 +326,7 @@ export class ScalarCardDataTable { } private getDataTableMode(): DataTableMode { - return this.stepOrLinkedTimeSelection.end + return this.stepOrLinkedTimeSelection?.end ? DataTableMode.RANGE : DataTableMode.SINGLE; } diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_fob_controller.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_fob_controller.ts index 7d676db9561..bb863024d72 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_fob_controller.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_fob_controller.ts @@ -52,13 +52,13 @@ import {MinMaxStep} from './scalar_card_types'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ScalarCardFobController { - @Input() timeSelection?: TimeSelection; + @Input() timeSelection: TimeSelection | undefined; @Input() scale!: Scale; @Input() minMaxHorizontalViewExtend!: [number, number]; @Input() minMaxStep!: MinMaxStep; @Input() axisSize!: number; @Input() disableInteraction: boolean = false; - @Input() allowFobRemoval?: boolean = true; + @Input() allowFobRemoval: boolean | undefined = true; @Output() onTimeSelectionChanged = new EventEmitter<{ timeSelection: TimeSelection; diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ng.html index 4faea6903d4..7ba88cdec84 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ng.html @@ -20,9 +20,9 @@ [seriesData]="seriesData" [seriesMetadataMap]="seriesMetadataMap" [xScaleType]="xScaleType" - [yScaleType]="yScaleType" + [yScaleType]="yScaleType ?? ScaleType.LINEAR" [customXFormatter]="getCustomXFormatter()" - [tooltipTemplate]="tooltipTemplate" + [tooltipTemplate]="tooltipTemplate ?? undefined" [ignoreYOutliers]="ignoreOutliers" [useDarkMode]="useDarkMode" [userViewBox]="userViewBox" diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ts index 96004cdb451..0a44b27c377 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_component.ts @@ -61,20 +61,20 @@ export class ScalarCardLineChartComponent { @Input() seriesMetadataMap!: ScalarCardSeriesMetadataMap; @Input() seriesData!: ScalarCardDataSeries[]; @Input() ignoreOutliers!: boolean; - @Input() disableUpdate!: boolean; + @Input() disableUpdate!: boolean | undefined; @Input() loadState!: DataLoadState; @Input() smoothingEnabled!: boolean; @Input() xAxisType!: XAxisType; @Input() xScaleType!: ScaleType; - @Input() yScaleType!: ScaleType; + @Input() yScaleType!: ScaleType | undefined; @Input() useDarkMode!: boolean; @Input() forceSvg!: boolean; @Input() stepOrLinkedTimeSelection: TimeSelection | undefined; @Input() minMaxStep!: MinMaxStep; @Input() userViewBox!: Extent | null; - @Input() tooltipTemplate!: TooltipTemplate | null; - @Input() allowFobRemoval!: boolean; - @Input() disableTooltip!: boolean; + @Input() tooltipTemplate!: TooltipTemplate | null | undefined; + @Input() allowFobRemoval!: boolean | undefined; + @Input() disableTooltip!: boolean | undefined; @Output() onTimeSelectionChanged = new EventEmitter<{ diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_container.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_container.ts index 75bb48c42e6..d4214f36336 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_container.ts @@ -20,6 +20,7 @@ import { OnInit, ViewChild, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {Observable, of, Subject} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -68,16 +69,16 @@ import {TimeSelectionView} from './utils'; ) { - this.useDarkMode$ = this.store.select(getDarkModeEnabled); + this.useDarkMode = this.store.selectSignal(getDarkModeEnabled); this.tooltipSort$ = this.store.select(getMetricsTooltipSort); - this.forceSvg$ = this.store.select(getForceSvgFeatureFlag); - this.ignoreOutliers$ = this.ignoreOutliers - ? of(this.ignoreOutliers) - : this.store.select(getMetricsIgnoreOutliers); - this.xAxisType$ = this.xAxisType - ? of(this.xAxisType) - : this.store.select(getMetricsXAxisType); - this.xScaleType$ = this.xAxisType - ? of(ScaleType.LINEAR) - : this.store.select(getMetricsXAxisType).pipe( - map((xAxisType) => { - switch (xAxisType) { - case XAxisType.STEP: - case XAxisType.RELATIVE: - return ScaleType.LINEAR; - case XAxisType.WALL_TIME: - return ScaleType.TIME; - default: - const neverType = xAxisType as never; - throw new Error( - `Invalid xAxisType for line chart. ${neverType}` - ); - } - }) - ); + this.forceSvg = this.store.selectSignal(getForceSvgFeatureFlag); + this.resolvedIgnoreOutliers = toSignal( + this.ignoreOutliers + ? of(this.ignoreOutliers) + : this.store.select(getMetricsIgnoreOutliers), + {requireSync: true} + ); + this.resolvedXAxisType = toSignal( + this.xAxisType + ? of(this.xAxisType) + : this.store.select(getMetricsXAxisType), + {requireSync: true} + ); + this.xScaleType = toSignal( + this.xAxisType + ? of(ScaleType.LINEAR) + : this.store.select(getMetricsXAxisType).pipe( + map((xAxisType) => { + switch (xAxisType) { + case XAxisType.STEP: + case XAxisType.RELATIVE: + return ScaleType.LINEAR; + case XAxisType.WALL_TIME: + return ScaleType.TIME; + default: + const neverType = xAxisType as never; + throw new Error( + `Invalid xAxisType for line chart. ${neverType}` + ); + } + }) + ), + {requireSync: true} + ); this.ngUnsubscribe = new Subject(); } @@ -151,14 +161,14 @@ export class ScalarCardLineChartContainer userViewBox$?: Observable; rangeEnabled$?: Observable; - readonly useDarkMode$; + readonly useDarkMode; readonly tooltipSort$; - readonly forceSvg$; + readonly forceSvg; - readonly ignoreOutliers$; + readonly resolvedIgnoreOutliers; - readonly xAxisType$; - readonly xScaleType$; + readonly resolvedXAxisType; + readonly xScaleType; private readonly ngUnsubscribe; diff --git a/tensorboard/webapp/metrics/views/card_renderer/vis_linked_time_selection_warning_component.ts b/tensorboard/webapp/metrics/views/card_renderer/vis_linked_time_selection_warning_component.ts index f7ef04e2cb5..8ecce361199 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/vis_linked_time_selection_warning_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/vis_linked_time_selection_warning_component.ts @@ -38,6 +38,6 @@ export type TimeSelectionWithClipped = TimeSelection & {clipped: boolean}; changeDetection: ChangeDetectionStrategy.OnPush, }) export class VisLinkedTimeSelectionWarningComponent { - @Input() isClipped?: boolean = false; - @Input() isClosestStepHighlighted?: boolean = false; + @Input() isClipped: boolean | undefined = false; + @Input() isClosestStepHighlighted: boolean | null | undefined = false; } diff --git a/tensorboard/webapp/metrics/views/main_view/card_grid_container.ts b/tensorboard/webapp/metrics/views/main_view/card_grid_container.ts index 3bf7394ab88..6697c3c56b9 100644 --- a/tensorboard/webapp/metrics/views/main_view/card_grid_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/card_grid_container.ts @@ -20,6 +20,7 @@ import { OnDestroy, SimpleChanges, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {BehaviorSubject, combineLatest, Observable, of, Subject} from 'rxjs'; import {map, shareReplay, switchMap, takeUntil, tap} from 'rxjs/operators'; @@ -38,14 +39,14 @@ import {CardIdWithMetadata} from '../metrics_view_types'; selector: 'metrics-card-grid', template: ` @@ -63,22 +64,25 @@ export class CardGridContainer implements OnChanges, OnDestroy { readonly pageIndex$ = new BehaviorSubject(0); private readonly items$ = new BehaviorSubject([]); private readonly ngUnsubscribe = new Subject(); - readonly cardStateMap$; + readonly cardStateMap; readonly numPages$; + readonly numPages; readonly isGroupExpanded$: Observable; + readonly isGroupExpanded; - readonly showPaginationControls$: Observable; + readonly showPaginationControls; readonly normalizedPageIndex$; + readonly normalizedPageIndex; - readonly pagedItems$; + readonly pagedItems; - readonly cardMinWidth$; + readonly cardMinWidth; constructor(private readonly store: Store) { - this.cardStateMap$ = this.store.select(selectors.getCardStateMap); + this.cardStateMap = this.store.selectSignal(selectors.getCardStateMap); this.numPages$ = combineLatest([ this.items$, this.store.select(settingsSelectors.getPageSize), @@ -87,6 +91,7 @@ export class CardGridContainer implements OnChanges, OnDestroy { return Math.ceil(items.length / pageSize); }) ); + this.numPages = toSignal(this.numPages$, {requireSync: true}); this.isGroupExpanded$ = this.groupName$.pipe( switchMap((groupName) => { return groupName !== null @@ -94,8 +99,12 @@ export class CardGridContainer implements OnChanges, OnDestroy { : of(true); }) ); - this.showPaginationControls$ = this.numPages$.pipe( - map((numPages) => numPages > 1) + this.isGroupExpanded = toSignal(this.isGroupExpanded$, { + requireSync: true, + }); + this.showPaginationControls = toSignal( + this.numPages$.pipe(map((numPages) => numPages > 1)), + {requireSync: true} ); this.normalizedPageIndex$ = combineLatest([ this.pageIndex$, @@ -119,7 +128,10 @@ export class CardGridContainer implements OnChanges, OnDestroy { }), shareReplay(1) ); - this.pagedItems$ = combineLatest([ + this.normalizedPageIndex = toSignal(this.normalizedPageIndex$, { + requireSync: true, + }); + const pagedItems$ = combineLatest([ this.items$, this.store.select(settingsSelectors.getPageSize), this.normalizedPageIndex$, @@ -131,7 +143,8 @@ export class CardGridContainer implements OnChanges, OnDestroy { return items.slice(startIndex, endIndex); }) ); - this.cardMinWidth$ = this.store.select(getMetricsCardMinWidth); + this.pagedItems = toSignal(pagedItems$, {requireSync: true}); + this.cardMinWidth = this.store.selectSignal(getMetricsCardMinWidth); } ngOnChanges(changes: SimpleChanges) { diff --git a/tensorboard/webapp/metrics/views/main_view/card_group_toolbar_container.ts b/tensorboard/webapp/metrics/views/main_view/card_group_toolbar_container.ts index 044c51a07bf..607c2b8f8c1 100644 --- a/tensorboard/webapp/metrics/views/main_view/card_group_toolbar_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/card_group_toolbar_container.ts @@ -12,9 +12,16 @@ 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Injector, + Input, + Signal, +} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable, of} from 'rxjs'; +import {of} from 'rxjs'; import {State} from '../../../app_state'; import {getMetricsTagGroupExpansionState} from '../../../selectors'; import {metricsTagGroupExpansionChanged} from '../../actions'; @@ -25,7 +32,7 @@ import {metricsTagGroupExpansionChanged} from '../../actions'; template: ` @@ -35,15 +42,22 @@ import {metricsTagGroupExpansionChanged} from '../../actions'; export class CardGroupToolBarContainer { @Input() groupName: string | null = null; @Input() numberOfCards!: number; - isGroupExpanded$: Observable = of(false); + isGroupExpanded!: Signal; - constructor(private readonly store: Store) {} + constructor( + private readonly store: Store, + private readonly injector: Injector + ) {} ngOnInit() { - this.isGroupExpanded$ = + const isGroupExpanded$ = this.groupName !== null ? this.store.select(getMetricsTagGroupExpansionState, this.groupName) : of(false); + this.isGroupExpanded = toSignal(isGroupExpanded$, { + injector: this.injector, + requireSync: true, + }); } onGroupExpansionToggled() { diff --git a/tensorboard/webapp/metrics/views/main_view/card_groups_container.ts b/tensorboard/webapp/metrics/views/main_view/card_groups_container.ts index 48a82965556..fede8af7de9 100644 --- a/tensorboard/webapp/metrics/views/main_view/card_groups_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/card_groups_container.ts @@ -12,9 +12,9 @@ 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {combineLatestWith, map} from 'rxjs/operators'; import {State} from '../../../app_state'; import {getMetricsFilteredPluginTypes} from '../../store'; @@ -28,7 +28,7 @@ import {getSortedRenderableCardIdsWithMetadata} from './common_selectors'; selector: 'metrics-card-groups', template: ` `, @@ -38,9 +38,8 @@ export class CardGroupsContainer { @Input() cardObserver!: CardObserver; constructor(private readonly store: Store) { - this.cardGroups$ = this.store - .select(getSortedRenderableCardIdsWithMetadata) - .pipe( + this.cardGroups = toSignal( + this.store.select(getSortedRenderableCardIdsWithMetadata).pipe( combineLatestWith(this.store.select(getMetricsFilteredPluginTypes)), map(([cardList, filteredPlugins]) => { if (!filteredPlugins.size) return cardList; @@ -49,8 +48,10 @@ export class CardGroupsContainer { }); }), map((cardList) => groupCardIdWithMetdata(cardList)) - ); + ), + {requireSync: true} + ); } - readonly cardGroups$: Observable; + readonly cardGroups: Signal; } diff --git a/tensorboard/webapp/metrics/views/main_view/empty_tag_match_message_container.ts b/tensorboard/webapp/metrics/views/main_view/empty_tag_match_message_container.ts index bd32f690b80..a1bb32d6fc8 100644 --- a/tensorboard/webapp/metrics/views/main_view/empty_tag_match_message_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/empty_tag_match_message_container.ts @@ -12,9 +12,9 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {State} from '../../../app_state'; import {PluginType} from '../../data_source'; @@ -29,27 +29,28 @@ import {getSortedRenderableCardIdsWithMetadata} from './common_selectors'; selector: 'metrics-empty-tag-match', template: ` `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class EmptyTagMatchMessageContainer { constructor(private readonly store: Store) { - this.pluginTypes$ = this.store.select(getMetricsFilteredPluginTypes); - this.tagFilterRegex$ = this.store.select(getMetricsTagFilter); - this.tagCounts$ = this.store - .select(getSortedRenderableCardIdsWithMetadata) - .pipe( + this.pluginTypes = this.store.selectSignal(getMetricsFilteredPluginTypes); + this.tagFilterRegex = this.store.selectSignal(getMetricsTagFilter); + this.tagCounts = toSignal( + this.store.select(getSortedRenderableCardIdsWithMetadata).pipe( map((cardList) => { return new Set(cardList.map(({tag}) => tag)).size; }) - ); + ), + {requireSync: true} + ); } - readonly pluginTypes$: Observable>; - readonly tagFilterRegex$: Observable; - readonly tagCounts$: Observable; + readonly pluginTypes: Signal>; + readonly tagFilterRegex: Signal; + readonly tagCounts: Signal; } diff --git a/tensorboard/webapp/metrics/views/main_view/filter_input_container.ts b/tensorboard/webapp/metrics/views/main_view/filter_input_container.ts index 77a3e0ba7b6..874b70704b2 100644 --- a/tensorboard/webapp/metrics/views/main_view/filter_input_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/filter_input_container.ts @@ -12,7 +12,8 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {Observable} from 'rxjs'; import {combineLatestWith, filter, map, startWith} from 'rxjs/operators'; @@ -30,9 +31,9 @@ import {compareTagNames} from '../../utils'; selector: 'metrics-tag-filter', template: ` `, @@ -41,54 +42,64 @@ import {compareTagNames} from '../../utils'; export class MetricsFilterInputContainer { constructor(private readonly store: Store) { this.tagFilter$ = this.store.select(getMetricsTagFilter); - this.isTagFilterRegexValid$ = this.tagFilter$.pipe( - map((tagFilterString) => { - try { - // tslint:disable-next-line:no-unused-expression Check for validity of filter. - new RegExp(tagFilterString); - return true; - } catch (err) { - return false; - } - }) - ); - this.completions$ = this.store.select(getNonEmptyCardIdsWithMetadata).pipe( - combineLatestWith(this.store.select(getMetricsFilteredPluginTypes)), - map(([cardList, filteredPluginTypes]) => { - return cardList - .filter(({plugin}) => { - return !filteredPluginTypes.size || filteredPluginTypes.has(plugin); - }) - .map(({tag}) => tag); - }), - // De-duplicate using Set since Image cards has a notion of Sample and - // the same `run` and `tag` can appear more than once. - map((tags) => [...new Set(tags)]), - map((tags) => tags.sort(compareTagNames)), - combineLatestWith(this.store.select(getMetricsTagFilter)), - map<[string[], string], [string[], RegExp | null]>( - ([tags, tagFilter]) => { + this.tagFilter = this.store.selectSignal(getMetricsTagFilter); + this.isTagFilterRegexValid = toSignal( + this.tagFilter$.pipe( + map((tagFilterString) => { try { - const regex = new RegExp(tagFilter, 'i'); - return [tags, regex]; - } catch (e) { - return [tags, null]; + // tslint:disable-next-line:no-unused-expression Check for validity of filter. + new RegExp(tagFilterString); + return true; + } catch (err) { + return false; + } + }) + ), + {requireSync: true} + ); + this.completions = toSignal( + this.store.select(getNonEmptyCardIdsWithMetadata).pipe( + combineLatestWith(this.store.select(getMetricsFilteredPluginTypes)), + map(([cardList, filteredPluginTypes]) => { + return cardList + .filter(({plugin}) => { + return ( + !filteredPluginTypes.size || filteredPluginTypes.has(plugin) + ); + }) + .map(({tag}) => tag); + }), + // De-duplicate using Set since Image cards has a notion of Sample and + // the same `run` and `tag` can appear more than once. + map((tags) => [...new Set(tags)]), + map((tags) => tags.sort(compareTagNames)), + combineLatestWith(this.store.select(getMetricsTagFilter)), + map<[string[], string], [string[], RegExp | null]>( + ([tags, tagFilter]) => { + try { + const regex = new RegExp(tagFilter, 'i'); + return [tags, regex]; + } catch (e) { + return [tags, null]; + } } - } + ), + filter(([, tagFilterRegex]) => tagFilterRegex !== null), + map(([tags, tagFilterRegex]) => { + return tags.filter((tag: string) => tagFilterRegex!.test(tag)); + }), + startWith([] as string[]) ), - filter(([, tagFilterRegex]) => tagFilterRegex !== null), - map(([tags, tagFilterRegex]) => { - return tags.filter((tag: string) => tagFilterRegex!.test(tag)); - }), - startWith([] as string[]) + {requireSync: true} ); } readonly tagFilter$: Observable; + readonly tagFilter: Signal; - readonly isTagFilterRegexValid$: Observable; + readonly isTagFilterRegexValid: Signal; - readonly completions$: Observable; + readonly completions: Signal; onTagFilterChange(tagFilter: string) { this.store.dispatch(metricsTagFilterChanged({tagFilter})); diff --git a/tensorboard/webapp/metrics/views/main_view/filtered_view_container.ts b/tensorboard/webapp/metrics/views/main_view/filtered_view_container.ts index 5a6cf4c94e7..60059d271d4 100644 --- a/tensorboard/webapp/metrics/views/main_view/filtered_view_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/filtered_view_container.ts @@ -12,7 +12,8 @@ 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {Observable} from 'rxjs'; import { @@ -41,8 +42,8 @@ export const FILTER_VIEW_DEBOUNCE_IN_MS = 200; selector: 'metrics-filtered-view', template: ` `, @@ -88,17 +89,24 @@ export class FilteredViewContainer { share(), startWith([]) ) as Observable[]>; - this.isEmptyMatch$ = this.cardIdsWithMetadata$.pipe( - combineLatestWith( - this.store.select(getSortedRenderableCardIdsWithMetadata) + this.cardIdsWithMetadata = toSignal(this.cardIdsWithMetadata$, { + requireSync: true, + }); + this.isEmptyMatch = toSignal( + this.cardIdsWithMetadata$.pipe( + combineLatestWith( + this.store.select(getSortedRenderableCardIdsWithMetadata) + ), + map(([filteredCardList, fullCardList]) => { + return Boolean(fullCardList.length) && filteredCardList.length === 0; + }) ), - map(([filteredCardList, fullCardList]) => { - return Boolean(fullCardList.length) && filteredCardList.length === 0; - }) + {requireSync: true} ); } readonly cardIdsWithMetadata$: Observable[]>; + readonly cardIdsWithMetadata: Signal[]>; - readonly isEmptyMatch$: Observable; + readonly isEmptyMatch: Signal; } diff --git a/tensorboard/webapp/metrics/views/main_view/main_view_container.ts b/tensorboard/webapp/metrics/views/main_view/main_view_container.ts index 8e3d8551f76..b22a390b0a5 100644 --- a/tensorboard/webapp/metrics/views/main_view/main_view_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/main_view_container.ts @@ -12,9 +12,9 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map, takeWhile} from 'rxjs/operators'; import {State} from '../../../app_state'; import {DataLoadState} from '../../../types/data'; @@ -38,11 +38,11 @@ import {PluginType} from '../../types'; selector: 'metrics-main-view', template: ` ) { - this.isSidepaneOpen$ = this.store.select(isMetricsSettingsPaneOpen); - this.initialTagsLoading$ = this.store - .select(getMetricsTagMetadataLoadState) - .pipe( + this.isSidepaneOpen = this.store.selectSignal(isMetricsSettingsPaneOpen); + this.initialTagsLoading = toSignal( + this.store.select(getMetricsTagMetadataLoadState).pipe( // disconnect and don't listen to store if tags are loaded at least once. takeWhile((loadState) => { return loadState.lastLoadedTimeInMs === null; @@ -67,27 +66,34 @@ export class MainViewContainer { loadState.lastLoadedTimeInMs === null ); }) - ); - this.showFilteredView$ = this.store.select(getMetricsTagFilter).pipe( - map((filter) => { - return filter.length > 0; - }) + ), + {requireSync: true} ); - this.filteredPluginTypes$ = this.store.select( + this.showFilteredView = toSignal( + this.store.select(getMetricsTagFilter).pipe( + map((filter) => { + return filter.length > 0; + }) + ), + {requireSync: true} + ); + this.filteredPluginTypes = this.store.selectSignal( getMetricsFilteredPluginTypes ); - this.isSlideoutMenuOpen$ = this.store.select(isMetricsSlideoutMenuOpen); + this.isSlideoutMenuOpen = this.store.selectSignal( + isMetricsSlideoutMenuOpen + ); } - readonly isSidepaneOpen$: Observable; + readonly isSidepaneOpen: Signal; - readonly initialTagsLoading$: Observable; + readonly initialTagsLoading: Signal; - readonly showFilteredView$: Observable; + readonly showFilteredView: Signal; - readonly filteredPluginTypes$; + readonly filteredPluginTypes; - readonly isSlideoutMenuOpen$: Observable; + readonly isSlideoutMenuOpen: Signal; onSettingsButtonClicked() { this.store.dispatch(metricsSettingsPaneToggled()); diff --git a/tensorboard/webapp/metrics/views/main_view/main_view_test.ts b/tensorboard/webapp/metrics/views/main_view/main_view_test.ts index 3229b0cec39..d21872e1540 100644 --- a/tensorboard/webapp/metrics/views/main_view/main_view_test.ts +++ b/tensorboard/webapp/metrics/views/main_view/main_view_test.ts @@ -931,13 +931,7 @@ describe('metrics main view', () => { }); it('resets the card min width', () => { - const getMetricsCardMinWidthSubject = new ReplaySubject( - 1 - ); - getMetricsCardMinWidthSubject.next(500); - selectSpy - .withArgs(getMetricsCardMinWidth) - .and.returnValue(getMetricsCardMinWidthSubject); + store.overrideSelector(getMetricsCardMinWidth, 500); let fixture = TestBed.createComponent(MainViewContainer); fixture.detectChanges(); @@ -947,7 +941,8 @@ describe('metrics main view', () => { ] ).toBe('repeat(auto-fill, minmax(500px, 1fr))'); - getMetricsCardMinWidthSubject.next(null); + store.overrideSelector(getMetricsCardMinWidth, null); + store.refreshState(); fixture.detectChanges(); expect( diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts index f3122285ff6..819f8bdb241 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts @@ -74,7 +74,7 @@ import {CardIdWithMetadata} from '../metrics_view_types'; export class PinnedViewComponent { @Input() cardObserver!: CardObserver; @Input() cardIdsWithMetadata!: CardIdWithMetadata[]; - @Input() lastPinnedCardTime!: number; + @Input() lastPinnedCardTime: number | null = null; @Input() globalPinsEnabled: boolean = false; @Output() onClearAllPinsClicked = new EventEmitter(); } diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts index 2b49ea16d8f..d3c7826d99a 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts @@ -12,9 +12,9 @@ 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {map, skip, startWith} from 'rxjs/operators'; import {State} from '../../../app_state'; import {getEnableGlobalPins} from '../../../selectors'; @@ -29,10 +29,10 @@ import {CardIdWithMetadata} from '../metrics_view_types'; selector: 'metrics-pinned-view', template: ` `, @@ -42,25 +42,29 @@ export class PinnedViewContainer { @Input() cardObserver!: CardObserver; constructor(private readonly store: Store) { - this.cardIdsWithMetadata$ = this.store - .select(getPinnedCardsWithMetadata) - .pipe( + this.cardIdsWithMetadata = toSignal( + this.store.select(getPinnedCardsWithMetadata).pipe( map((cards) => cards as DeepReadonly[]), startWith([] as DeepReadonly[]) - ); + ), + {requireSync: true} + ); + // Genuinely async: skip(1) means no synchronous value on subscribe, so + // this stays on AsyncPipe. See lastPinnedCardTime's nullable widening in + // pinned_view_component.ts. this.lastPinnedCardTime$ = this.store.select(getLastPinnedCardTime).pipe( // Ignore the first value on component load, only reacting to new // pins after page load. skip(1) ); - this.globalPinsEnabled$ = this.store.select(getEnableGlobalPins); + this.globalPinsEnabled = this.store.selectSignal(getEnableGlobalPins); } - readonly cardIdsWithMetadata$: Observable[]>; + readonly cardIdsWithMetadata: Signal[]>; readonly lastPinnedCardTime$; - readonly globalPinsEnabled$; + readonly globalPinsEnabled; onClearAllPinsClicked() { this.store.dispatch(metricsClearAllPinnedCards()); diff --git a/tensorboard/webapp/metrics/views/metrics_container.ts b/tensorboard/webapp/metrics/views/metrics_container.ts index 14faf01b786..fd13c7701aa 100644 --- a/tensorboard/webapp/metrics/views/metrics_container.ts +++ b/tensorboard/webapp/metrics/views/metrics_container.ts @@ -25,7 +25,7 @@ import {getRunsTableFullScreen} from '../../core/store/core_selectors'; `, @@ -33,9 +33,9 @@ import {getRunsTableFullScreen} from '../../core/store/core_selectors'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MetricsDashboardContainer { - runsTableFullScreen$; + runsTableFullScreen; constructor(readonly store: Store) { - this.runsTableFullScreen$ = this.store.select(getRunsTableFullScreen); + this.runsTableFullScreen = this.store.selectSignal(getRunsTableFullScreen); } } diff --git a/tensorboard/webapp/metrics/views/right_pane/scalar_column_editor/scalar_column_editor_container.ts b/tensorboard/webapp/metrics/views/right_pane/scalar_column_editor/scalar_column_editor_container.ts index 5421ce2df0d..bad0b8041f4 100644 --- a/tensorboard/webapp/metrics/views/right_pane/scalar_column_editor/scalar_column_editor_container.ts +++ b/tensorboard/webapp/metrics/views/right_pane/scalar_column_editor/scalar_column_editor_container.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {map} from 'rxjs/operators'; import {State} from '../../../../app_state'; @@ -42,9 +43,9 @@ function headersWithoutRuns(headers: ColumnHeader[]) { selector: 'metrics-scalar-column-editor', template: ` ) { - this.singleHeaders$ = this.store - .select(getSingleSelectionHeaders) - .pipe(map(headersWithoutRuns)); - this.rangeHeaders$ = this.store - .select(getRangeSelectionHeaders) - .pipe(map(headersWithoutRuns)); - this.selectedTab$ = this.store.select(getTableEditorSelectedTab); + this.singleHeaders = toSignal( + this.store + .select(getSingleSelectionHeaders) + .pipe(map(headersWithoutRuns)), + {requireSync: true} + ); + this.rangeHeaders = toSignal( + this.store.select(getRangeSelectionHeaders).pipe(map(headersWithoutRuns)), + {requireSync: true} + ); + this.selectedTab = this.store.selectSignal(getTableEditorSelectedTab); } - readonly singleHeaders$; - readonly rangeHeaders$; - readonly selectedTab$; + readonly singleHeaders; + readonly rangeHeaders; + readonly selectedTab; onScalarTableColumnToggled(toggleInfo: HeaderToggleInfo) { this.store.dispatch(dataTableColumnToggled(toggleInfo)); diff --git a/tensorboard/webapp/metrics/views/right_pane/settings_view_component.ts b/tensorboard/webapp/metrics/views/right_pane/settings_view_component.ts index d18b352e22e..e90c6345615 100644 --- a/tensorboard/webapp/metrics/views/right_pane/settings_view_component.ts +++ b/tensorboard/webapp/metrics/views/right_pane/settings_view_component.ts @@ -82,7 +82,7 @@ export class SettingsViewComponent { @Output() onSlideOutToggled = new EventEmitter(); @Output() onEnableSavingPinsToggled = new EventEmitter(); - @Input() isImageSupportEnabled!: boolean; + @Input() isImageSupportEnabled: boolean | null = null; readonly TooltipSortDropdownOptions: DropdownOption[] = [ {value: TooltipSort.ALPHABETICAL, displayText: 'Alphabetical'}, @@ -133,7 +133,7 @@ export class SettingsViewComponent { readonly MAX_CARD_WIDTH_SLIDER_VALUE = MAX_CARD_WIDTH_SLIDER_VALUE; readonly MIN_CARD_WIDTH_SLIDER_VALUE = MIN_CARD_WIDTH_SLIDER_VALUE; readonly cardWidthSliderChanged$ = new EventEmitter(); - @Input() cardMinWidth!: number; + @Input() cardMinWidth!: number | null; @Output() cardWidthChanged = this.cardWidthSliderChanged$.pipe( auditTime(SLIDER_AUDIT_TIME_MS) diff --git a/tensorboard/webapp/metrics/views/right_pane/settings_view_container.ts b/tensorboard/webapp/metrics/views/right_pane/settings_view_container.ts index 905ae49bc01..e856147a9b5 100644 --- a/tensorboard/webapp/metrics/views/right_pane/settings_view_container.ts +++ b/tensorboard/webapp/metrics/views/right_pane/settings_view_container.ts @@ -12,10 +12,9 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; import {filter, map, take, withLatestFrom} from 'rxjs/operators'; import {State} from '../../../app_state'; import * as selectors from '../../../selectors'; @@ -54,51 +53,49 @@ import { template: ` `, @@ -109,25 +106,29 @@ export class SettingsViewContainer { private readonly store: Store, private readonly dialog: MatDialog ) { - this.isScalarStepSelectorEnabled$ = this.store.select( + this.isScalarStepSelectorEnabled = this.store.selectSignal( selectors.getMetricsStepSelectorEnabled ); - this.isScalarStepSelectorRangeEnabled$ = this.store.select( + this.isScalarStepSelectorRangeEnabled = this.store.selectSignal( selectors.getMetricsRangeSelectionEnabled ); - this.isLinkedTimeEnabled$ = this.store.select( + this.isLinkedTimeEnabled = this.store.selectSignal( selectors.getMetricsLinkedTimeEnabled ); - this.isScalarColumnCustomizationEnabled$ = this.store.select( + this.isScalarColumnCustomizationEnabled = this.store.selectSignal( selectors.getIsScalarColumnCustomizationEnabled ); - this.linkedTimeSelection$ = this.store.select( + this.linkedTimeSelection = this.store.selectSignal( selectors.getMetricsLinkedTimeSelectionSetting ); - this.stepMinMax$ = this.store.select(selectors.getMetricsStepMinMax); - this.isSlideOutMenuOpen$ = this.store.select( + this.stepMinMax = this.store.selectSignal(selectors.getMetricsStepMinMax); + this.isSlideOutMenuOpen = this.store.selectSignal( selectors.isMetricsSlideoutMenuOpen ); + // Genuinely async: filter(Boolean) + take(1) waits for feature flags to + // load, so there is no synchronous value on subscribe. Stays on + // AsyncPipe; see isImageSupportEnabled's nullable widening in + // settings_view_component.ts. this.isImageSupportEnabled$ = this.store .select(selectors.getIsFeatureFlagsLoaded) .pipe( @@ -140,67 +141,71 @@ export class SettingsViewContainer { return isImagesSupported; }) ); - this.tooltipSort$ = this.store.select(selectors.getMetricsTooltipSort); - this.ignoreOutliers$ = this.store.select( + this.tooltipSort = this.store.selectSignal(selectors.getMetricsTooltipSort); + this.ignoreOutliers = this.store.selectSignal( selectors.getMetricsIgnoreOutliers ); - this.isTooltipRowsLimitEnabled$ = this.store.select( + this.isTooltipRowsLimitEnabled = this.store.selectSignal( selectors.getMetricsIsTooltipRowsLimitEnabled ); - this.tooltipRowsLimit$ = this.store.select( + this.tooltipRowsLimit = this.store.selectSignal( selectors.getMetricsTooltipRowsLimit ); - this.xAxisType$ = this.store.select(selectors.getMetricsXAxisType); - this.cardMinWidth$ = this.store.select(selectors.getMetricsCardMinWidth); - this.histogramMode$ = this.store.select(selectors.getMetricsHistogramMode); - this.scalarSmoothing$ = this.store.select( + this.xAxisType = this.store.selectSignal(selectors.getMetricsXAxisType); + this.cardMinWidth = this.store.selectSignal( + selectors.getMetricsCardMinWidth + ); + this.histogramMode = this.store.selectSignal( + selectors.getMetricsHistogramMode + ); + this.scalarSmoothing = this.store.selectSignal( selectors.getMetricsScalarSmoothing ); - this.scalarPartitionX$ = this.store.select( + this.scalarPartitionX = this.store.selectSignal( selectors.getMetricsScalarPartitionNonMonotonicX ); - this.imageBrightnessInMilli$ = this.store.select( + this.imageBrightnessInMilli = this.store.selectSignal( selectors.getMetricsImageBrightnessInMilli ); - this.imageContrastInMilli$ = this.store.select( + this.imageContrastInMilli = this.store.selectSignal( selectors.getMetricsImageContrastInMilli ); - this.imageShowActualSize$ = this.store.select( + this.imageShowActualSize = this.store.selectSignal( selectors.getMetricsImageShowActualSize ); - this.isSavingPinsEnabled$ = this.store.select( + this.isSavingPinsEnabled = this.store.selectSignal( selectors.getMetricsSavingPinsEnabled ); - this.globalPinsFeatureEnabled$ = this.store.select( + this.globalPinsFeatureEnabled = this.store.selectSignal( selectors.getEnableGlobalPins ); } - readonly isScalarStepSelectorEnabled$: Observable; - readonly isScalarStepSelectorRangeEnabled$: Observable; - readonly isLinkedTimeEnabled$: Observable; - readonly isScalarColumnCustomizationEnabled$; - readonly linkedTimeSelection$; - readonly stepMinMax$; - readonly isSlideOutMenuOpen$; + readonly isScalarStepSelectorEnabled: Signal; + readonly isScalarStepSelectorRangeEnabled: Signal; + readonly isLinkedTimeEnabled: Signal; + readonly isScalarColumnCustomizationEnabled; + readonly linkedTimeSelection; + readonly stepMinMax; + readonly isSlideOutMenuOpen; readonly isImageSupportEnabled$; - readonly tooltipSort$; - readonly ignoreOutliers$; - readonly isTooltipRowsLimitEnabled$; - readonly tooltipRowsLimit$; - readonly xAxisType$; - readonly cardMinWidth$; - readonly histogramMode$; - readonly scalarSmoothing$; - readonly scalarPartitionX$; - readonly imageBrightnessInMilli$; - readonly imageContrastInMilli$; - readonly imageShowActualSize$; - readonly isSavingPinsEnabled$; + readonly tooltipSort; + readonly ignoreOutliers; + readonly isTooltipRowsLimitEnabled; + readonly tooltipRowsLimit; + readonly xAxisType; + readonly cardMinWidth; + readonly histogramMode; + readonly scalarSmoothing; + readonly scalarPartitionX; + readonly imageBrightnessInMilli; + readonly imageContrastInMilli; + readonly imageShowActualSize; + readonly isSavingPinsEnabled; // Feature flag for global pins. - readonly globalPinsFeatureEnabled$; + readonly globalPinsFeatureEnabled; onTooltipSortChanged(sort: TooltipSort) { this.store.dispatch(metricsChangeTooltipSort({sort})); diff --git a/tensorboard/webapp/notification_center/_views/notification_center_container.ts b/tensorboard/webapp/notification_center/_views/notification_center_container.ts index 94fb9ee783f..ccbdc37564f 100644 --- a/tensorboard/webapp/notification_center/_views/notification_center_container.ts +++ b/tensorboard/webapp/notification_center/_views/notification_center_container.ts @@ -12,9 +12,10 @@ 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 {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Signal} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; -import {combineLatest, Observable} from 'rxjs'; +import {combineLatest} from 'rxjs'; import {map, shareReplay} from 'rxjs/operators'; import {State} from '../../app_state'; import * as actions from '../_redux/notification_center_actions'; @@ -33,19 +34,19 @@ const iconMap = new Map([[CategoryEnum.WHATS_NEW, 'info_outline_24px']]); selector: 'notification-center', template: ` `, }) export class NotificationCenterContainer { - readonly notificationNotes$: Observable; + readonly notificationNotes: Signal; - readonly hasUnreadMessages$; + readonly hasUnreadMessages: Signal; constructor(private readonly store: Store) { - this.notificationNotes$ = combineLatest([ + const notificationNotes$ = combineLatest([ this.store.select(getNotifications), this.store.select(getLastReadTime), ]).pipe( @@ -60,10 +61,16 @@ export class NotificationCenterContainer { }), shareReplay() ); - this.hasUnreadMessages$ = this.notificationNotes$.pipe( - map((notifications) => { - return notifications.some(({hasRead}) => !hasRead); - }) + this.notificationNotes = toSignal(notificationNotes$, { + requireSync: true, + }); + this.hasUnreadMessages = toSignal( + notificationNotes$.pipe( + map((notifications) => { + return notifications.some(({hasRead}) => !hasRead); + }) + ), + {requireSync: true} ); } diff --git a/tensorboard/webapp/plugins/plugins_component.ts b/tensorboard/webapp/plugins/plugins_component.ts index d697fdc24aa..3245da79ad8 100644 --- a/tensorboard/webapp/plugins/plugins_component.ts +++ b/tensorboard/webapp/plugins/plugins_component.ts @@ -100,16 +100,16 @@ export class PluginsComponent implements OnChanges { featureFlags!: FeatureFlags; @Input() - lastUpdated?: number; + lastUpdated: number | null | undefined; @Input() - environmentFailureNotFoundTemplate?: TemplateRef; + environmentFailureNotFoundTemplate: TemplateRef | undefined; @Input() - environmentFailurePermissionDeniedTemplate?: TemplateRef; + environmentFailurePermissionDeniedTemplate: TemplateRef | undefined; @Input() - environmentFailureUnknownTemplate?: TemplateRef; + environmentFailureUnknownTemplate: TemplateRef | undefined; readonly PluginLoadState = PluginLoadState; readonly LoadingMechanismType = LoadingMechanismType; diff --git a/tensorboard/webapp/plugins/plugins_container.ts b/tensorboard/webapp/plugins/plugins_container.ts index 774d3a60219..98132803da3 100644 --- a/tensorboard/webapp/plugins/plugins_container.ts +++ b/tensorboard/webapp/plugins/plugins_container.ts @@ -18,6 +18,7 @@ import { Input, TemplateRef, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {createSelector, Store} from '@ngrx/store'; import {combineLatest} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -57,14 +58,14 @@ const activePlugin = createSelector( selector: 'plugins', template: ` ; @@ -88,65 +89,73 @@ export class PluginsContainer { @Input() environmentFailureUnknownTemplate?: TemplateRef; - readonly pluginLoadState$; + readonly pluginLoadState; - readonly lastLoadedTimeInMs$; - readonly dataLocation$; - readonly isFeatureFlagsLoaded$; - readonly featureFlags$; - readonly settingsLoadState$; + readonly lastLoadedTimeInMs; + readonly dataLocation; + readonly isFeatureFlagsLoaded; + readonly featureFlags; + readonly settingsLoadState; constructor(private readonly store: Store) { - this.activeKnownPlugin$ = this.store.select(activePlugin); - this.activePluginId$ = this.store.select(getActivePlugin); - this.pluginLoadState$ = combineLatest( - this.activeKnownPlugin$, - this.activePluginId$, - this.store.select(getPluginsListLoaded) - ).pipe( - map(([activePlugin, activePluginId, loadState]) => { - if (loadState.failureCode !== null) { - // Despite its 'Plugins'-specific name, getPluginsListLoaded actually - // encapsulates multiple requests to load different parts of the - // environment. - if (loadState.failureCode === PluginsListFailureCode.NOT_FOUND) { - return PluginLoadState.ENVIRONMENT_FAILURE_NOT_FOUND; - } else if ( - loadState.failureCode === PluginsListFailureCode.PERMISSION_DENIED - ) { - return PluginLoadState.ENVIRONMENT_FAILURE_PERMISSION_DENIED; - } else { - return PluginLoadState.ENVIRONMENT_FAILURE_UNKNOWN; + this.activeKnownPlugin = this.store.selectSignal(activePlugin); + this.activePluginId = this.store.selectSignal(getActivePlugin); + this.pluginLoadState = toSignal( + combineLatest( + this.store.select(activePlugin), + this.store.select(getActivePlugin), + this.store.select(getPluginsListLoaded) + ).pipe( + map(([activePlugin, activePluginId, loadState]) => { + if (loadState.failureCode !== null) { + // Despite its 'Plugins'-specific name, getPluginsListLoaded + // actually encapsulates multiple requests to load different + // parts of the environment. + if (loadState.failureCode === PluginsListFailureCode.NOT_FOUND) { + return PluginLoadState.ENVIRONMENT_FAILURE_NOT_FOUND; + } else if ( + loadState.failureCode === PluginsListFailureCode.PERMISSION_DENIED + ) { + return PluginLoadState.ENVIRONMENT_FAILURE_PERMISSION_DENIED; + } else { + return PluginLoadState.ENVIRONMENT_FAILURE_UNKNOWN; + } } - } - if (activePlugin !== null) { - return PluginLoadState.LOADED; - } + if (activePlugin !== null) { + return PluginLoadState.LOADED; + } - if ( - loadState.lastLoadedTimeInMs === null && - loadState.state === DataLoadState.LOADING - ) { - return PluginLoadState.LOADING; - } + if ( + loadState.lastLoadedTimeInMs === null && + loadState.state === DataLoadState.LOADING + ) { + return PluginLoadState.LOADING; + } - if (activePluginId) { - return PluginLoadState.UNKNOWN_PLUGIN_ID; - } + if (activePluginId) { + return PluginLoadState.UNKNOWN_PLUGIN_ID; + } - return PluginLoadState.NO_ENABLED_PLUGINS; - }) + return PluginLoadState.NO_ENABLED_PLUGINS; + }) + ), + {requireSync: true} + ); + this.lastLoadedTimeInMs = this.store.selectSignal(getAppLastLoadedTimeInMs); + this.dataLocation = toSignal( + this.store.select(getEnvironment).pipe( + map((env) => { + return env.data_location; + }) + ), + {requireSync: true} ); - this.lastLoadedTimeInMs$ = this.store.select(getAppLastLoadedTimeInMs); - this.dataLocation$ = this.store.select(getEnvironment).pipe( - map((env) => { - return env.data_location; - }) + this.isFeatureFlagsLoaded = this.store.selectSignal( + getIsFeatureFlagsLoaded ); - this.isFeatureFlagsLoaded$ = this.store.select(getIsFeatureFlagsLoaded); - this.featureFlags$ = this.store.select(getFeatureFlags); - this.settingsLoadState$ = this.store.select( + this.featureFlags = this.store.selectSignal(getFeatureFlags); + this.settingsLoadState = this.store.selectSignal( settingsSelectors.getSettingsLoadState ); } diff --git a/tensorboard/webapp/runs/views/runs_selector/runs_selector_container.ts b/tensorboard/webapp/runs/views/runs_selector/runs_selector_container.ts index 681b1a2a368..12c798f88f7 100644 --- a/tensorboard/webapp/runs/views/runs_selector/runs_selector_container.ts +++ b/tensorboard/webapp/runs/views/runs_selector/runs_selector_container.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {map} from 'rxjs/operators'; import {State} from '../../../app_state'; @@ -24,29 +25,35 @@ import {RunsTableColumn} from '../runs_table/types'; selector: 'runs-selector', template: ` `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class RunsSelectorContainer { - readonly experimentIds$; - readonly columns$; + readonly experimentIds; + readonly columns; constructor(private readonly store: Store) { - this.experimentIds$ = this.store - .select(getExperimentIdsFromRoute) - .pipe(map((experimentIdsOrNull) => experimentIdsOrNull ?? [])); - this.columns$ = this.store.select(getExperimentIdsFromRoute).pipe( - map((ids) => { - return [ - RunsTableColumn.CHECKBOX, - RunsTableColumn.RUN_NAME, - ids && ids.length > 1 ? RunsTableColumn.EXPERIMENT_NAME : null, - RunsTableColumn.RUN_COLOR, - ].filter((col) => col !== null) as RunsTableColumn[]; - }) + this.experimentIds = toSignal( + this.store + .select(getExperimentIdsFromRoute) + .pipe(map((experimentIdsOrNull) => experimentIdsOrNull ?? [])), + {requireSync: true} + ); + this.columns = toSignal( + this.store.select(getExperimentIdsFromRoute).pipe( + map((ids) => { + return [ + RunsTableColumn.CHECKBOX, + RunsTableColumn.RUN_NAME, + ids && ids.length > 1 ? RunsTableColumn.EXPERIMENT_NAME : null, + RunsTableColumn.RUN_COLOR, + ].filter((col) => col !== null) as RunsTableColumn[]; + }) + ), + {requireSync: true} ); } } diff --git a/tensorboard/webapp/runs/views/runs_table/filterbar_component.ng.html b/tensorboard/webapp/runs/views/runs_table/filterbar_component.ng.html index d9a317e7e23..306e162cbc6 100644 --- a/tensorboard/webapp/runs/views/runs_table/filterbar_component.ng.html +++ b/tensorboard/webapp/runs/views/runs_table/filterbar_component.ng.html @@ -33,7 +33,7 @@ @@ -35,12 +35,12 @@ import {FilterAddedEvent} from '../../../widgets/data_table/types'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FilterbarContainer implements OnDestroy { - filters$; + filters; private readonly ngUnsubscribe = new Subject(); constructor(private readonly store: Store) { - this.filters$ = this.store.select( + this.filters = this.store.selectSignal( hparamsSelectors.getDashboardHparamFilterMap ); } diff --git a/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_container.ts b/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_container.ts index d0eb31bb0e1..18c8f6a2067 100644 --- a/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_container.ts +++ b/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_container.ts @@ -12,7 +12,13 @@ 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 {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + Signal, +} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {Store} from '@ngrx/store'; import {combineLatest, defer, merge, Observable, Subject} from 'rxjs'; @@ -50,10 +56,10 @@ const INPUT_CHANGE_DEBOUNCE_INTERVAL_MS = 500; standalone: false, selector: 'regex-edit-dialog', template: `>; private readonly allRuns$: Observable; private readonly expNameByExpId$: Observable>; - readonly enableColorByExperiment$: Observable; + readonly enableColorByExperiment; // Tentative regex string and type are used because we don't want to change the state // every time we type in or select the dropdown option. @@ -86,75 +92,84 @@ export class RegexEditDialogContainer { private readonly tentativeRegexType$: Subject = new Subject(); - readonly groupByRegexString$: Observable = defer(() => { + private readonly groupByRegexStringObs$: Observable = defer(() => { return merge( this.store.select(getColorGroupRegexString).pipe(take(1)), this.tentativeRegexString$ ); }).pipe(startWith(''), shareReplay(1)); - readonly groupByRegexType$: Observable; - - readonly colorRunPairList$: Observable = defer(() => { - return this.groupByRegexString$.pipe( - debounceTime(INPUT_CHANGE_DEBOUNCE_INTERVAL_MS), - filter((regexString) => { - try { - const regex = new RegExp(regexString); - return Boolean(regex); - } catch (e) { - return false; - } - }), - combineLatestWith( - this.groupByRegexType$, - this.allRuns$, - this.runIdToEid$, - this.expNameByExpId$, - this.store.select(settingsSelectors.getColorPalette), - this.store.select(getDarkModeEnabled) - ), - map( - ([ - regexString, - regexType, - allRuns, - runIdToEid, - expNameByExpId, - colorPalette, - darkModeEnabled, - ]) => { - const groupBy = { - key: regexType, + readonly groupByRegexString = toSignal(this.groupByRegexStringObs$, { + requireSync: true, + }); + + private readonly groupByRegexTypeObs$: Observable; + readonly groupByRegexType: Signal; + + private readonly colorRunPairListObs$: Observable = defer( + () => { + return this.groupByRegexStringObs$.pipe( + debounceTime(INPUT_CHANGE_DEBOUNCE_INTERVAL_MS), + filter((regexString) => { + try { + const regex = new RegExp(regexString); + return Boolean(regex); + } catch (e) { + return false; + } + }), + combineLatestWith( + this.groupByRegexTypeObs$, + this.allRuns$, + this.runIdToEid$, + this.expNameByExpId$, + this.store.select(settingsSelectors.getColorPalette), + this.store.select(getDarkModeEnabled) + ), + map( + ([ regexString, - }; - const groups = groupRuns( - groupBy, + regexType, allRuns, runIdToEid, - expNameByExpId - ); - const groupKeyToColorString = new Map(); - const colorRunPairList: ColorGroup[] = []; - - for (const [groupId, runs] of Object.entries(groups.matches)) { - let colorHex: string | undefined = - groupKeyToColorString.get(groupId); - if (!colorHex) { - const color = - colorPalette.colors[ - groupKeyToColorString.size % colorPalette.colors.length - ]; - colorHex = darkModeEnabled ? color.darkHex : color.lightHex; - groupKeyToColorString.set(groupId, colorHex); + expNameByExpId, + colorPalette, + darkModeEnabled, + ]) => { + const groupBy = { + key: regexType, + regexString, + }; + const groups = groupRuns( + groupBy, + allRuns, + runIdToEid, + expNameByExpId + ); + const groupKeyToColorString = new Map(); + const colorRunPairList: ColorGroup[] = []; + + for (const [groupId, runs] of Object.entries(groups.matches)) { + let colorHex: string | undefined = + groupKeyToColorString.get(groupId); + if (!colorHex) { + const color = + colorPalette.colors[ + groupKeyToColorString.size % colorPalette.colors.length + ]; + colorHex = darkModeEnabled ? color.darkHex : color.lightHex; + groupKeyToColorString.set(groupId, colorHex); + } + colorRunPairList.push({groupId, color: colorHex, runs}); } - colorRunPairList.push({groupId, color: colorHex, runs}); + return colorRunPairList; } - return colorRunPairList; - } - ) - ); - }).pipe(startWith([])); + ) + ); + } + ).pipe(startWith([])); + + readonly colorRunPairList: Signal; constructor() { const data = inject<{ @@ -162,10 +177,10 @@ export class RegexEditDialogContainer { }>(MAT_DIALOG_DATA); this.expNameByExpId$ = this.store.select(getDashboardExperimentNames); - this.enableColorByExperiment$ = this.store.select( + this.enableColorByExperiment = this.store.selectSignal( getEnableColorByExperiment ); - this.groupByRegexType$ = merge( + this.groupByRegexTypeObs$ = merge( this.store.select(getRunGroupBy).pipe( take(1), map((group) => group.key) @@ -207,6 +222,16 @@ export class RegexEditDialogContainer { return runsList.flat(); }) ); + + // Assigned last: both derived signals subscribe eagerly, and depend on + // fields assigned above (groupByRegexTypeObs$, allRuns$, runIdToEid$, + // expNameByExpId$) via the lazy `defer()` in colorRunPairListObs$. + this.groupByRegexType = toSignal(this.groupByRegexTypeObs$, { + requireSync: true, + }); + this.colorRunPairList = toSignal(this.colorRunPairListObs$, { + requireSync: true, + }); } onRegexInputOnChange(regexString: string) { @@ -219,8 +244,8 @@ export class RegexEditDialogContainer { onSave(): void { combineLatest([ - this.groupByRegexString$, - this.groupByRegexType$, + this.groupByRegexStringObs$, + this.groupByRegexTypeObs$, this.expNameByExpId$, ]).subscribe(([regexString, key, expNameByExpId]) => { if (regexString) { diff --git a/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_test.ts b/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_test.ts index 04ad9200d90..55b82147532 100644 --- a/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_test.ts +++ b/tensorboard/webapp/runs/views/runs_table/regex_edit_dialog_test.ts @@ -81,13 +81,20 @@ describe('regex_edit_dialog', () => { store?.resetSelectors(); }); - function createComponent(experimentIds: string[]) { + function createComponent( + experimentIds: string[], + regexString = 'test regex string' + ) { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: {experimentIds}, }); store = TestBed.inject>(Store) as MockStore; - store.overrideSelector(getColorGroupRegexString, 'test regex string'); + // `getColorGroupRegexString` is only ever read once, via `take(1)`, at + // component construction time (the container intentionally treats it as + // a one-shot initial value, not a live binding) — so it must be set + // before `TestBed.createComponent()` runs below, not overridden after. + store.overrideSelector(getColorGroupRegexString, regexString); store.overrideSelector(getRuns, []); store.overrideSelector(getRunIdsForExperiment, []); store.overrideSelector(getDarkModeEnabled, false); @@ -330,12 +337,13 @@ describe('regex_edit_dialog', () => { }); it('fills example and generates group preview', fakeAsync(() => { - const fixture = createComponent(['rose']); + const fixture = createComponent(['rose'], 'run'); store.overrideSelector(getRuns, [ buildRun({id: 'run1', name: 'run 1'}), buildRun({id: 'run2', name: 'run 2'}), ]); - store.overrideSelector(getColorGroupRegexString, 'run'); + store.overrideSelector(getRunIdsForExperiment, ['run1', 'run2']); + store.refreshState(); fixture.detectChanges(); tick(TEST_ONLY.INPUT_CHANGE_DEBOUNCE_INTERVAL_MS); fixture.detectChanges(); @@ -357,12 +365,13 @@ describe('regex_edit_dialog', () => { describe('live grouping result preview', () => { it('renders grouping result based on regex in store', fakeAsync(() => { - const fixture = createComponent(['rose']); + const fixture = createComponent(['rose'], 'run'); store.overrideSelector(getRuns, [ buildRun({id: 'run1', name: 'run 1'}), buildRun({id: 'run2', name: 'run 2'}), ]); - store.overrideSelector(getColorGroupRegexString, 'run'); + store.overrideSelector(getRunIdsForExperiment, ['run1', 'run2']); + store.refreshState(); fixture.detectChanges(); tick(TEST_ONLY.INPUT_CHANGE_DEBOUNCE_INTERVAL_MS); fixture.detectChanges(); @@ -382,6 +391,7 @@ describe('regex_edit_dialog', () => { buildRun({id: 'run2', name: 'run 2'}), ]); store.overrideSelector(getRunIdsForExperiment, ['run1', 'run2']); + store.refreshState(); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); @@ -412,6 +422,7 @@ describe('regex_edit_dialog', () => { buildRun({id: 'run2', name: 'run2 name'}), ]); store.overrideSelector(getRunIdsForExperiment, ['run1', 'run2']); + store.refreshState(); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); @@ -515,6 +526,7 @@ describe('regex_edit_dialog', () => { buildRun({id: 'run2', name: 'run 2'}), ]); store.overrideSelector(getRunIdsForExperiment, ['run1', 'run2']); + store.refreshState(); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); @@ -568,6 +580,7 @@ describe('regex_edit_dialog', () => { 'run6', 'run7', ]); + store.refreshState(); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); diff --git a/tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html b/tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html index 5cc1d95354f..7f1a1650b9c 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html +++ b/tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html @@ -32,7 +32,7 @@ [numColumnsLoaded]="numColumnsLoaded" [hasMoreColumnsToLoad]="numColumnsLoaded === numColumnsToLoad" [columnFilters]="columnFilters" - [loading]="loading" + [loading]="loading ?? false" [shouldAddBorders]="true" (sortDataBy)="sortDataBy.emit($event)" (orderColumns)="orderColumns.emit($event)" diff --git a/tensorboard/webapp/runs/views/runs_table/runs_data_table.ts b/tensorboard/webapp/runs/views/runs_table/runs_data_table.ts index 9f207d3986d..03038f5ccc4 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_data_table.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_data_table.ts @@ -47,7 +47,7 @@ export class RunsDataTable { @Input() selectableColumns!: ColumnHeader[]; @Input() numColumnsLoaded!: number; @Input() numColumnsToLoad!: number; - @Input() loading!: boolean; + @Input() loading: boolean | null = null; @Input() columnFilters!: Map; ColumnHeaderType = ColumnHeaderType; diff --git a/tensorboard/webapp/runs/views/runs_table/runs_group_menu_button_container.ts b/tensorboard/webapp/runs/views/runs_table/runs_group_menu_button_container.ts index a858b9338c5..948cb959454 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_group_menu_button_container.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_group_menu_button_container.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {Store} from '@ngrx/store'; import {Observable} from 'rxjs'; import {filter, map, shareReplay, startWith, take} from 'rxjs/operators'; @@ -38,10 +39,10 @@ import {GroupBy, GroupByKey} from '../../types'; selector: 'runs-group-menu-button', template: ` @@ -52,32 +53,36 @@ export class RunsGroupMenuButtonContainer { @Input() experimentIds!: string[]; constructor(private readonly store: Store) { - this.showExperimentsGroupBy$ = this.store - .select(getRegisteredRouteKinds) - .pipe( + this.showExperimentsGroupBy = toSignal( + this.store.select(getRegisteredRouteKinds).pipe( map((registeredRouteKinds) => { return registeredRouteKinds.has(RouteKind.COMPARE_EXPERIMENT); }) - ); - this.selectedGroupBy$ = this.store.select(getRunGroupBy); - this.lastRegexGroupByKey$ = this.store.select(getRunGroupBy).pipe( - map((group) => group.key), - filter( - (key) => key === GroupByKey.REGEX || key === GroupByKey.REGEX_BY_EXP ), - startWith(GroupByKey.REGEX) + {requireSync: true} + ); + this.selectedGroupBy = this.store.selectSignal(getRunGroupBy); + this.lastRegexGroupByKey = toSignal( + this.store.select(getRunGroupBy).pipe( + map((group) => group.key), + filter( + (key) => key === GroupByKey.REGEX || key === GroupByKey.REGEX_BY_EXP + ), + startWith(GroupByKey.REGEX) + ), + {requireSync: true} ); - this.groupByRegexString$ = this.store.select(getColorGroupRegexString); + this.groupByRegexString = this.store.selectSignal(getColorGroupRegexString); this.expNameByExpId$ = this.store.select(getDashboardExperimentNames); } - readonly showExperimentsGroupBy$: Observable; + readonly showExperimentsGroupBy; - readonly selectedGroupBy$: Observable; + readonly selectedGroupBy; - readonly lastRegexGroupByKey$: Observable; + readonly lastRegexGroupByKey; - readonly groupByRegexString$: Observable; + readonly groupByRegexString; readonly expNameByExpId$: Observable>; diff --git a/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts b/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts index 018ae6c21b5..21792cb1162 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts @@ -15,10 +15,13 @@ limitations under the License. import { ChangeDetectionStrategy, Component, + Injector, Input, OnDestroy, OnInit, + Signal, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {createSelector, Store} from '@ngrx/store'; import {combineLatest, Observable, of, Subject} from 'rxjs'; import { @@ -92,16 +95,16 @@ const getRunsLoading = createSelector< selector: 'runs-table', template: ` = of([]); - loading$: Observable | null = null; - sortingInfo$; + sortedRunsTableData!: Signal; + loading!: Signal; + private readonly sortingInfoObs$; + sortingInfo; // Column to disable in the table. The columns are rendered in the order as // defined by this input. @@ -142,31 +146,35 @@ export class RunsTableContainer implements OnInit, OnDestroy { @Input() experimentIds!: string[]; - regexFilter$; - runsColumns$; - selectableColumns$; - numColumnsLoaded$; - numColumnsToLoad$; + regexFilter; + runsColumns; + selectableColumns; + numColumnsLoaded; + numColumnsToLoad; - columnFilters$; + columnFilters; allRunsTableData$; private readonly ngUnsubscribe; - constructor(private readonly store: Store) { - this.sortingInfo$ = this.store.select(getRunsTableSortingInfo); + constructor( + private readonly store: Store, + private readonly injector: Injector + ) { + this.sortingInfoObs$ = this.store.select(getRunsTableSortingInfo); + this.sortingInfo = toSignal(this.sortingInfoObs$, {requireSync: true}); this.columns = [RunsTableColumn.RUN_NAME]; - this.regexFilter$ = this.store.select(getRunSelectorRegexFilter); - this.runsColumns$ = this.store.select(getGroupedRunsTableHeaders); - this.selectableColumns$ = this.store.select(getSelectableColumns); - this.numColumnsLoaded$ = this.store.select( + this.regexFilter = this.store.selectSignal(getRunSelectorRegexFilter); + this.runsColumns = this.store.selectSignal(getGroupedRunsTableHeaders); + this.selectableColumns = this.store.selectSignal(getSelectableColumns); + this.numColumnsLoaded = this.store.selectSignal( hparamsSelectors.getNumDashboardHparamsLoaded ); - this.numColumnsToLoad$ = this.store.select( + this.numColumnsToLoad = this.store.selectSignal( hparamsSelectors.getNumDashboardHparamsToLoad ); - this.columnFilters$ = this.store.select(getCurrentColumnFilters); + this.columnFilters = this.store.selectSignal(getCurrentColumnFilters); this.allRunsTableData$ = this.store.select(getFilteredRenderableRuns).pipe( map((filteredRenderableRuns) => { return filteredRenderableRuns.map((runTableItem) => { @@ -191,13 +199,13 @@ export class RunsTableContainer implements OnInit, OnDestroy { this.getRunTableItemsForExperiment(id) ); - this.sortedRunsTableData$ = combineLatest([ - this.allRunsTableData$, - this.sortingInfo$, - ]).pipe( - map(([items, sortingInfo]) => { - return sortTableDataItems(items, sortingInfo); - }) + this.sortedRunsTableData = toSignal( + combineLatest([this.allRunsTableData$, this.sortingInfoObs$]).pipe( + map(([items, sortingInfo]) => { + return sortTableDataItems(items, sortingInfo); + }) + ), + {injector: this.injector, requireSync: true} ); const rawAllUnsortedRunTableItems$ = combineLatest( @@ -212,11 +220,19 @@ export class RunsTableContainer implements OnInit, OnDestroy { const getRunsLoadingPerExperiment = this.experimentIds.map((id) => { return this.store.select(getRunsLoading, {experimentId: id}); }); - this.loading$ = combineLatest(getRunsLoadingPerExperiment).pipe( - map((experimentsLoading) => { - return experimentsLoading.some((isLoading) => isLoading); - }) - ); + // combineLatest([]) never emits, so short-circuit when there are no + // experiments rather than breaking the requireSync guarantee below. + const loading$ = getRunsLoadingPerExperiment.length + ? combineLatest(getRunsLoadingPerExperiment).pipe( + map((experimentsLoading) => { + return experimentsLoading.some((isLoading) => isLoading); + }) + ) + : of(false); + this.loading = toSignal(loading$, { + injector: this.injector, + requireSync: true, + }); /** * For consumers who show checkboxes, notify users that new runs may not be diff --git a/tensorboard/webapp/settings/_views/settings_button_container.ts b/tensorboard/webapp/settings/_views/settings_button_container.ts index b938b0ad20d..beb1bbd47a1 100644 --- a/tensorboard/webapp/settings/_views/settings_button_container.ts +++ b/tensorboard/webapp/settings/_views/settings_button_container.ts @@ -23,14 +23,14 @@ import {State} from '../_redux/settings_types'; selector: 'settings-button', template: ` `, }) export class SettingsButtonContainer { - readonly settingsLoadState$; + readonly settingsLoadState; constructor(private store: Store) { - this.settingsLoadState$ = this.store.select(getSettingsLoadState); + this.settingsLoadState = this.store.selectSignal(getSettingsLoadState); } } diff --git a/tensorboard/webapp/settings/_views/settings_dialog_container.ts b/tensorboard/webapp/settings/_views/settings_dialog_container.ts index 6ebd189c880..0f9eb98f3b5 100644 --- a/tensorboard/webapp/settings/_views/settings_dialog_container.ts +++ b/tensorboard/webapp/settings/_views/settings_dialog_container.ts @@ -32,9 +32,9 @@ import {State} from '../_redux/settings_types'; selector: 'settings-dialog', template: ` ) { - this.reloadEnabled$ = this.store.select(getReloadEnabled); - this.reloadPeriodInMs$ = this.store.select(getReloadPeriodInMs); - this.pageSize$ = this.store.select(getPageSize); + this.reloadEnabled = this.store.selectSignal(getReloadEnabled); + this.reloadPeriodInMs = this.store.selectSignal(getReloadPeriodInMs); + this.pageSize = this.store.selectSignal(getPageSize); } onReloadToggled(): void { diff --git a/tensorboard/webapp/testing/BUILD b/tensorboard/webapp/testing/BUILD index d3a43a487f9..fbbad5ccab0 100644 --- a/tensorboard/webapp/testing/BUILD +++ b/tensorboard/webapp/testing/BUILD @@ -114,8 +114,10 @@ tf_ng_module( "//tensorboard/webapp/app_routing:types", "//tensorboard/webapp/core", "//tensorboard/webapp/deeplink:testing", + "//tensorboard/webapp/experiments", "//tensorboard/webapp/feature_flag", "//tensorboard/webapp/runs", + "//tensorboard/webapp/settings", "@npm//@angular/core", "@npm//@ngrx/effects", "@npm//@ngrx/store", diff --git a/tensorboard/webapp/testing/integration_test_module.ts b/tensorboard/webapp/testing/integration_test_module.ts index 585403d7fd1..736025001b3 100644 --- a/tensorboard/webapp/testing/integration_test_module.ts +++ b/tensorboard/webapp/testing/integration_test_module.ts @@ -26,8 +26,10 @@ import {RouteRegistryModule} from '../app_routing/route_registry_module'; import {RouteKind} from '../app_routing/types'; import {CoreModule} from '../core/core_module'; import {TestableNoopHashDeepLinkerModule} from '../deeplink/testing'; +import {ExperimentsModule} from '../experiments/experiments_module'; import {FeatureFlagModule} from '../feature_flag/feature_flag_module'; import {RunsModule} from '../runs/runs_module'; +import {SettingsModule} from '../settings/settings_module'; import {MatIconTestingModule} from './mat_icon_module'; @Component({ @@ -56,7 +58,9 @@ export function provideRoute(): RouteDef[] { FeatureFlagModule, CoreModule, AppRoutingModule, + ExperimentsModule, RunsModule, + SettingsModule, TestableNoopHashDeepLinkerModule, RouteRegistryModule.registerRoutes(provideRoute), NgrxStoreModule.forRoot([]), diff --git a/tensorboard/webapp/widgets/card_fob/card_fob_controller_component.ts b/tensorboard/webapp/widgets/card_fob/card_fob_controller_component.ts index f23bd3092b6..b56856fbc26 100644 --- a/tensorboard/webapp/widgets/card_fob/card_fob_controller_component.ts +++ b/tensorboard/webapp/widgets/card_fob/card_fob_controller_component.ts @@ -55,7 +55,7 @@ export class CardFobControllerComponent { @ViewChild('prospectiveFobWrapper') readonly prospectiveFobWrapper!: ElementRef; @Input() axisDirection!: AxisDirection; - @Input() timeSelection?: TimeSelection; + @Input() timeSelection: TimeSelection | undefined; @Input() cardFobHelper!: CardFobGetStepFromPositionHelper; @Input() startStepAxisPosition!: number; @Input() endStepAxisPosition!: number | null; @@ -64,7 +64,7 @@ export class CardFobControllerComponent { @Input() showExtendedLine?: Boolean = false; @Input() prospectiveStep: number | null = null; @Input() prospectiveStepAxisPosition?: number | null = null; - @Input() allowFobRemoval?: boolean = true; + @Input() allowFobRemoval: boolean | undefined = true; @Output() onTimeSelectionChanged = new EventEmitter(); diff --git a/tensorboard/webapp/widgets/data_table/context_menu_component.ts b/tensorboard/webapp/widgets/data_table/context_menu_component.ts index 990797d796d..f73a31ff154 100644 --- a/tensorboard/webapp/widgets/data_table/context_menu_component.ts +++ b/tensorboard/webapp/widgets/data_table/context_menu_component.ts @@ -31,7 +31,7 @@ import {ColumnHeader, Side, SortingInfo, SortingOrder} from './types'; }) export class ContextMenuComponent { @Input() contextMenuHeader: ColumnHeader | undefined = undefined; - @Input() selectableColumns?: ColumnHeader[]; + @Input() selectableColumns: ColumnHeader[] | undefined; @Input() sortingInfo!: SortingInfo; @Output() removeColumn = new EventEmitter(); diff --git a/tensorboard/webapp/widgets/data_table/data_table_component.ng.html b/tensorboard/webapp/widgets/data_table/data_table_component.ng.html index c641da6dd32..ca7f12346d1 100644 --- a/tensorboard/webapp/widgets/data_table/data_table_component.ng.html +++ b/tensorboard/webapp/widgets/data_table/data_table_component.ng.html @@ -26,7 +26,7 @@ ; + customVisTemplate: TemplateRef | undefined; @Input() - customChartOverlayTemplate?: TemplateRef< - TemplateContext & {interactionState: InteractionState} - >; + customChartOverlayTemplate: + | TemplateRef + | undefined; @Input() useDarkMode: boolean = false; @@ -118,7 +118,7 @@ export class LineChartComponent // In case of PR curve line chart, we do not want to compute the viewBox based on the // data. @Input() - fixedViewBox?: Extent; + fixedViewBox: Extent | undefined; @Input() seriesMetadataMap!: DataSeriesMetadataMap; @@ -130,20 +130,20 @@ export class LineChartComponent yScaleType: ScaleType = ScaleType.LINEAR; @Input() - customXFormatter?: Formatter; + customXFormatter: Formatter | undefined; @Input() - customYFormatter?: Formatter; + customYFormatter: Formatter | undefined; @Input() - tooltipTemplate?: TooltipTemplate; + tooltipTemplate: TooltipTemplate | undefined; @Input() userViewBox: Extent | null = null; @Input() - lineOnly?: boolean = false; + lineOnly: boolean | undefined = false; - @Input() disableTooltip?: boolean = false; + @Input() disableTooltip: boolean | undefined = false; @Output() viewBoxChanged = new EventEmitter(); @@ -156,7 +156,7 @@ export class LineChartComponent * changed and applies the change when the update is enabled. */ @Input() - disableUpdate?: boolean; + disableUpdate: boolean | undefined; /** * Whether to ignore outlier when computing default viewBox from the dataSeries. diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_axis_view.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_axis_view.ts index 7d4185cd139..669c872cce7 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_axis_view.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_axis_view.ts @@ -53,7 +53,7 @@ export class LineChartAxisComponent { domDim!: Dimension; @Input() - customFormatter?: Formatter; + customFormatter: Formatter | undefined; @Output() onViewExtentChange = new EventEmitter<[number, number]>(); diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts index 13a95c8f551..fb0cd29e702 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts @@ -132,9 +132,9 @@ export class LineChartInteractiveViewComponent tooltipOriginEl!: CdkOverlayOrigin; @Input() - tooltipTemplate?: TooltipTemplate; + tooltipTemplate: TooltipTemplate | undefined; - @Input() disableTooltip?: boolean; + @Input() disableTooltip: boolean | undefined; @Output() onViewExtentChange = new EventEmitter<{dataExtent: Extent}>(); diff --git a/tsconfig.json b/tsconfig.json index 3887c1e0529..a4df328d428 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,8 +33,6 @@ }, "angularCompilerOptions": { "strictTemplates": true, - // TODO(@cdavalos): Set this back to true after these containers stop using AsyncPipe - // for inputs. AsyncPipe can return null, so Angular currently rejects these bindings. - "strictNullInputTypes": false + "strictNullInputTypes": true } }