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'},
+ ]);
+ });
+ });
});