diff --git a/tensorboard/webapp/runs/views/runs_table/BUILD b/tensorboard/webapp/runs/views/runs_table/BUILD index 0d48ea0a46..3a5a0c619f 100644 --- a/tensorboard/webapp/runs/views/runs_table/BUILD +++ b/tensorboard/webapp/runs/views/runs_table/BUILD @@ -82,6 +82,7 @@ tf_ng_module( "//tensorboard/webapp/app_routing", "//tensorboard/webapp/app_routing:types", "//tensorboard/webapp/experiments:types", + "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/hparams", "//tensorboard/webapp/hparams:types", "//tensorboard/webapp/runs:types", @@ -95,6 +96,8 @@ tf_ng_module( "//tensorboard/webapp/types:ui", "//tensorboard/webapp/util:colors", "//tensorboard/webapp/util:matcher", + "//tensorboard/webapp/widgets/data_table", + "//tensorboard/webapp/widgets/data_table:types", "//tensorboard/webapp/widgets/experiment_alias", "//tensorboard/webapp/widgets/filter_input", "//tensorboard/webapp/widgets/range_input", @@ -135,6 +138,7 @@ tf_ts_library( "//tensorboard/webapp/app_routing:testing", "//tensorboard/webapp/app_routing:types", "//tensorboard/webapp/experiments/store:testing", + "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/hparams", "//tensorboard/webapp/hparams:testing", "//tensorboard/webapp/hparams:types", @@ -150,6 +154,7 @@ tf_ts_library( "//tensorboard/webapp/testing:utils", "//tensorboard/webapp/types", "//tensorboard/webapp/types:ui", + "//tensorboard/webapp/widgets/data_table", "//tensorboard/webapp/widgets/experiment_alias", "//tensorboard/webapp/widgets/filter_input", "//tensorboard/webapp/widgets/range_input", 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 43c098dfb5..566b0f248e 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_table_container.ts @@ -20,7 +20,7 @@ import { OnInit, } from '@angular/core'; import {createSelector, Store} from '@ngrx/store'; -import {combineLatest, Observable, of, Subject} from 'rxjs'; +import {BehaviorSubject, combineLatest, Observable, of, Subject} from 'rxjs'; import { distinctUntilChanged, filter, @@ -61,6 +61,12 @@ import { import {DataLoadState, LoadState} from '../../../types/data'; import {SortDirection} from '../../../types/ui'; import {matchRunToRegex} from '../../../util/matcher'; +import {getEnableHparamsInTimeSeries} from '../../../feature_flag/store/feature_flag_selectors'; +import { + ColumnHeaderType, + SortingOrder, + TableData, +} from '../../../widgets/data_table/types'; import { runColorChanged, runPageSelectionToggled, @@ -194,6 +200,7 @@ function matchFilter( selector: 'runs-table', template: ` + `, host: { '[class.flex-layout]': 'useFlexibleLayout', @@ -239,12 +256,30 @@ function matchFilter( }) export class RunsTableContainer implements OnInit, OnDestroy { private allUnsortedRunTableItems$?: Observable; + allRunsTableData$: Observable = of([]); loading$: Observable | null = null; filteredItemsLength$?: Observable; allItemsLength$?: Observable; pageItems$?: Observable; numSelectedItems$?: Observable; + // TODO(jameshollyer): Move these values to ngrx and make these Observables. + runsColumns = [ + { + type: ColumnHeaderType.RUN, + name: 'run', + displayName: 'Run', + enabled: true, + }, + ]; + sortingInfo = { + header: ColumnHeaderType.RUN, + name: 'run', + order: SortingOrder.ASCENDING, + }; + columnCustomizationEnabled = true; + smoothingEnabled = false; + hparamColumns$: Observable = of([]); metricColumns$: Observable = of([]); @@ -275,6 +310,7 @@ export class RunsTableContainer implements OnInit, OnDestroy { sortOption$ = this.store.select(getRunSelectorSort); paginationOption$ = this.store.select(getRunSelectorPaginationOption); regexFilter$ = this.store.select(getRunSelectorRegexFilter); + HParamsEnabled = new BehaviorSubject(false); private readonly ngUnsubscribe = new Subject(); constructor(private readonly store: Store) {} @@ -286,10 +322,24 @@ export class RunsTableContainer implements OnInit, OnDestroy { } ngOnInit() { + this.store.select(getEnableHparamsInTimeSeries).subscribe((enabled) => { + this.HParamsEnabled.next(enabled); + }); const getRunTableItemsPerExperiment = this.experimentIds.map((id) => this.getRunTableItemsForExperiment(id) ); + const getRunTableDataPerExperiment$ = this.experimentIds.map((id) => + this.getRunTableDataForExperiment(id) + ); + + this.allRunsTableData$ = combineLatest(getRunTableDataPerExperiment$).pipe( + map((itemsForExperiments: TableData[][]) => { + const items = [] as TableData[]; + return items.concat(...itemsForExperiments); + }) + ); + const rawAllUnsortedRunTableItems$ = combineLatest( getRunTableItemsPerExperiment ).pipe( @@ -519,6 +569,34 @@ export class RunsTableContainer implements OnInit, OnDestroy { return slicedItems; } + private getRunTableDataForExperiment( + experimentId: string + ): Observable { + return combineLatest([ + this.store.select(getRuns, {experimentId}), + this.store.select(getRunColorMap), + ]).pipe( + map(([runs, colorMap]) => { + return runs.map((run) => { + const tableData: TableData = { + id: run.id, + color: colorMap[run.id], + }; + this.runsColumns.forEach((column) => { + switch (column.type) { + case ColumnHeaderType.RUN: + tableData[column.name!] = run.name; + break; + default: + break; + } + }); + return tableData; + }); + }) + ); + } + private getRunTableItemsForExperiment( experimentId: string ): Observable { diff --git a/tensorboard/webapp/runs/views/runs_table/runs_table_module.ts b/tensorboard/webapp/runs/views/runs_table/runs_table_module.ts index 91f82aaa87..b141ee4ba9 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_table_module.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_table_module.ts @@ -31,6 +31,7 @@ import {MatSortModule} from '@angular/material/sort'; import {MatTableModule} from '@angular/material/table'; import {ColorPickerModule} from 'ngx-color-picker'; import {AlertModule} from '../../../alert/alert_module'; +import {DataTableModule} from '../../../widgets/data_table/data_table_module'; import {ExperimentAliasModule} from '../../../widgets/experiment_alias/experiment_alias_module'; import {FilterInputModule} from '../../../widgets/filter_input/filter_input_module'; import {RangeInputModule} from '../../../widgets/range_input/range_input_module'; @@ -45,6 +46,7 @@ import {RunsTableContainer} from './runs_table_container'; imports: [ ColorPickerModule, CommonModule, + DataTableModule, ExperimentAliasModule, FilterInputModule, MatFormFieldModule, diff --git a/tensorboard/webapp/runs/views/runs_table/runs_table_test.ts b/tensorboard/webapp/runs/views/runs_table/runs_table_test.ts index aa92ec4fbb..57509b353f 100644 --- a/tensorboard/webapp/runs/views/runs_table/runs_table_test.ts +++ b/tensorboard/webapp/runs/views/runs_table/runs_table_test.ts @@ -44,6 +44,7 @@ import {buildExperimentRouteFromId} from '../../../app_routing/testing'; import {RouteKind} from '../../../app_routing/types'; import {State} from '../../../app_state'; import {buildExperiment} from '../../../experiments/store/testing'; +import {getEnableHparamsInTimeSeries} from '../../../feature_flag/store/feature_flag_selectors'; import { actions as hparamsActions, selectors as hparamsSelectors, @@ -79,6 +80,8 @@ import {MatIconTestingModule} from '../../../testing/mat_icon_module'; import {provideMockTbStore} from '../../../testing/utils'; import {DataLoadState} from '../../../types/data'; import {SortDirection} from '../../../types/ui'; +import {DataTableModule} from '../../../widgets/data_table/data_table_module'; +import {DataTableComponent} from '../../../widgets/data_table/data_table_component'; import {ExperimentAliasModule} from '../../../widgets/experiment_alias/experiment_alias_module'; import {FilterInputModule} from '../../../widgets/filter_input/filter_input_module'; import {RangeInputModule} from '../../../widgets/range_input/range_input_module'; @@ -239,6 +242,7 @@ describe('runs_table', () => { FilterInputModule, RangeInputModule, ExperimentAliasModule, + DataTableModule, ], declarations: [ RunsGroupMenuButtonComponent, @@ -305,6 +309,7 @@ describe('runs_table', () => { settingsSelectors.getColorPalette, buildColorPalette() ); + store.overrideSelector(getEnableHparamsInTimeSeries, false); dispatchSpy = spyOn(store, 'dispatch').and.callFake((action: Action) => { actualActions.push(action); }); @@ -3169,4 +3174,51 @@ describe('runs_table', () => { }); }); }); + + describe('runs data table', () => { + beforeEach(() => { + store.overrideSelector(getEnableHparamsInTimeSeries, true); + }); + + it('renders data table when hparam flag is on', () => { + const fixture = createComponent(['book']); + fixture.detectChanges(); + + expect( + fixture.debugElement.query(By.directive(DataTableComponent)) + ).toBeTruthy(); + expect( + fixture.nativeElement.querySelector('runs-table-component') + ).toBeFalsy(); + }); + + it('passes run name and color to data table', () => { + // To make sure we only return the runs when called with the right props. + const selectSpy = spyOn(store, 'select').and.callThrough(); + selectSpy + .withArgs(getRuns, {experimentId: 'book'}) + .and.returnValue( + of([ + buildRun({id: 'book1', name: "The Philosopher's Stone"}), + buildRun({id: 'book2', name: 'The Chamber Of Secrets'}), + ]) + ); + + store.overrideSelector(getRunColorMap, { + book1: '#000', + book2: '#111', + }); + + const fixture = createComponent(['book']); + fixture.detectChanges(); + const dataTableComponent = fixture.debugElement.query( + By.directive(DataTableComponent) + ); + + expect(dataTableComponent.componentInstance.data).toEqual([ + {id: 'book1', color: '#000', run: "The Philosopher's Stone"}, + {id: 'book2', color: '#111', run: 'The Chamber Of Secrets'}, + ]); + }); + }); });