Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ export class ScalarCardDataTable {
}
}

function makeValueSortable(value: number | string | null | undefined) {
function makeValueSortable(
value: number | string | boolean | null | undefined
) {
if (
Number.isNaN(value) ||
value === 'NaN' ||
Expand Down
29 changes: 21 additions & 8 deletions tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@
[header]="header"
[sortingInfo]="sortingInfo"
[hparamsEnabled]="true"
[controlsEnabled]="header.type !== ColumnHeaderType.COLOR"
></tb-data-table-header-cell> </ng-container
[controlsEnabled]="header.type !== ColumnHeaderType.COLOR && header.type !== ColumnHeaderType.CUSTOM"
>
<div *ngIf="header.name === 'selected'">
<mat-checkbox
[checked]="allRowsSelected()"
[indeterminate]="!allRowsSelected() && someRowsSelected()"
(click)="onAllSelectionToggle.emit(getRunIds())"
></mat-checkbox>
</div>
</tb-data-table-header-cell> </ng-container
></ng-container>

<ng-container content>
Expand All @@ -42,12 +50,17 @@
[header]="header"
[datum]="dataRow[header.name]"
>
<div
*ngIf="header.type === ColumnHeaderType.COLOR"
class="row-circle"
>
<span [style.backgroundColor]="dataRow['color']"></span>
</div>
<ng-container [ngSwitch]="header.name">
<div *ngSwitchCase="'color'" class="row-circle">
<span [style.backgroundColor]="dataRow['color']"></span>
</div>
<div *ngSwitchCase="'selected'">
<mat-checkbox
[checked]="dataRow['selected']"
(change)="onSelectionToggle.emit(dataRow.id)"
></mat-checkbox>
</div>
</ng-container>
</tb-data-table-content-cell>
</ng-container>
</tb-data-table-content-row>
Expand Down
31 changes: 27 additions & 4 deletions tensorboard/webapp/runs/views/runs_table/runs_data_table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,38 @@ export class RunsDataTable {

@Output() sortDataBy = new EventEmitter<SortingInfo>();
@Output() orderColumns = new EventEmitter<ColumnHeader[]>();
@Output() onSelectionToggle = new EventEmitter<string>();
@Output() onAllSelectionToggle = new EventEmitter<string[]>();

getHeaders() {
return this.headers.concat([
return [
{
name: 'color',
name: 'selected',
displayName: '',
type: ColumnHeaderType.COLOR,
type: ColumnHeaderType.CUSTOM,
enabled: true,
},
]);
].concat(
this.headers.concat([
{
name: 'color',
displayName: '',
type: ColumnHeaderType.COLOR,
enabled: true,
},
])
);
}

getRunIds() {
return this.data.map((row) => row.id);
}

allRowsSelected() {
return this.data.every((row) => row.selected);
}

someRowsSelected() {
return this.data.some((row) => row.selected);
}
}
114 changes: 103 additions & 11 deletions tensorboard/webapp/runs/views/runs_table/runs_data_table_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {TestBed} from '@angular/core/testing';
import {RunsDataTable} from './runs_data_table';
import {DataTableModule} from '../../../widgets/data_table/data_table_module';
import {MatIconTestingModule} from '../../../testing/mat_icon_module';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {
SortingOrder,
SortingInfo,
Expand All @@ -39,6 +40,8 @@ import {ContentCellComponent} from '../../../widgets/data_table/content_cell_com
[sortingInfo]="sortingInfo"
(sortDataBy)="sortDataBy($event)"
(orderColumns)="orderColumns($event)"
(onSelectionToggle)="onSelectionToggle($event)"
(onAllSelectionToggle)="onAllSelectionToggle($event)"
></runs-data-table>
`,
})
Expand All @@ -49,9 +52,14 @@ class TestableComponent {
@Input() headers!: ColumnHeader[];
@Input() data!: TableData[];
@Input() sortingInfo!: SortingInfo;

@Input() onSelectionToggle!: (runId: string) => void;
@Input() onAllSelectionToggle!: (runIds: string[]) => void;
}

describe('runs_data_table', () => {
let onSelectionToggleSpy: jasmine.Spy;
let onAllSelectionToggleSpy: jasmine.Spy;
function createComponent(input: {
data?: TableData[];
headers?: ColumnHeader[];
Expand Down Expand Up @@ -88,13 +96,19 @@ describe('runs_data_table', () => {
order: SortingOrder.ASCENDING,
};

onSelectionToggleSpy = jasmine.createSpy();
fixture.componentInstance.onSelectionToggle = onSelectionToggleSpy;

onAllSelectionToggleSpy = jasmine.createSpy();
fixture.componentInstance.onAllSelectionToggle = onAllSelectionToggleSpy;

fixture.detectChanges();
return fixture;
}

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DataTableModule, MatIconTestingModule],
imports: [DataTableModule, MatIconTestingModule, MatCheckboxModule],
declarations: [TestableComponent, RunsDataTable],
}).compileComponents();
});
Expand All @@ -106,20 +120,21 @@ describe('runs_data_table', () => {
).toBeTruthy();
});

it('projects enabled headers plus color column', () => {
it('projects enabled headers plus color and selected column', () => {
const fixture = createComponent({});
const dataTable = fixture.debugElement.query(
By.directive(DataTableComponent)
);
const headers = dataTable.queryAll(By.directive(HeaderCellComponent));

expect(headers.length).toBe(3);
expect(headers[0].componentInstance.header.name).toEqual('run');
expect(headers[1].componentInstance.header.name).toEqual('other_header');
expect(headers[2].componentInstance.header.name).toEqual('color');
expect(headers.length).toBe(4);
expect(headers[0].componentInstance.header.name).toEqual('selected');
expect(headers[1].componentInstance.header.name).toEqual('run');
expect(headers[2].componentInstance.header.name).toEqual('other_header');
expect(headers[3].componentInstance.header.name).toEqual('color');
});

it('projects content for each enabled header and color column', () => {
it('projects content for each enabled header, selected, and color column', () => {
const fixture = createComponent({
data: [{id: 'runid', run: 'run name', color: 'red', other_header: 'foo'}],
});
Expand All @@ -128,10 +143,11 @@ describe('runs_data_table', () => {
);
const cells = dataTable.queryAll(By.directive(ContentCellComponent));

expect(cells.length).toBe(3);
expect(cells[0].componentInstance.header.name).toEqual('run');
expect(cells[1].componentInstance.header.name).toEqual('other_header');
expect(cells[2].componentInstance.header.name).toEqual('color');
expect(cells.length).toBe(4);
expect(cells[0].componentInstance.header.name).toEqual('selected');
expect(cells[1].componentInstance.header.name).toEqual('run');
expect(cells[2].componentInstance.header.name).toEqual('other_header');
expect(cells[3].componentInstance.header.name).toEqual('color');
});

it('disables controls for color header', () => {
Expand All @@ -148,4 +164,80 @@ describe('runs_data_table', () => {

expect(colorHeader.componentInstance.controlsEnabled).toBe(false);
});

it('disables controls for selected header', () => {
const fixture = createComponent({});

const dataTable = fixture.debugElement.query(
By.directive(DataTableComponent)
);
const headers = dataTable.queryAll(By.directive(HeaderCellComponent));

const selectedHeader = headers.find(
(h) => h.componentInstance.header.name === 'selected'
)!;

expect(selectedHeader.componentInstance.controlsEnabled).toBe(false);
});

it('adds checkbox to selected column', () => {
const fixture = createComponent({});

const dataTable = fixture.debugElement.query(
By.directive(DataTableComponent)
);
const headers = dataTable.queryAll(By.directive(HeaderCellComponent));
const cells = dataTable.queryAll(By.directive(ContentCellComponent));

const selectedHeader = headers.find(
(h) => h.componentInstance.header.name === 'selected'
)!;

const selectedContentCells = cells.filter((cell) => {
return cell.componentInstance.header.name === 'selected';
});

expect(selectedHeader.query(By.css('mat-checkbox'))).toBeTruthy();
selectedContentCells.forEach((cell) => {
expect(cell.query(By.css('mat-checkbox'))).toBeTruthy();
});
});

it('emits onAllSelectionToggle event when selected header checkbox is clicked', () => {
const fixture = createComponent({});

const dataTable = fixture.debugElement.query(
By.directive(DataTableComponent)
);
const headers = dataTable.queryAll(By.directive(HeaderCellComponent));

const selectedHeader = headers.find(
(h) => h.componentInstance.header.name === 'selected'
)!;

const selectedCheckbox = selectedHeader.query(By.css('mat-checkbox'));

selectedCheckbox.nativeElement.dispatchEvent(new MouseEvent('click'));

expect(onAllSelectionToggleSpy).toHaveBeenCalledWith(['runid']);
});

it('emits onSelectionToggle event when selected content checkbox is clicked', () => {
const fixture = createComponent({});

const dataTable = fixture.debugElement.query(
By.directive(DataTableComponent)
);
const cells = dataTable.queryAll(By.directive(ContentCellComponent));

const selectedContentCells = cells.filter((cell) => {
return cell.componentInstance.header.name === 'selected';
});

const firstCheckbox = selectedContentCells[0].query(By.css('mat-checkbox'));

firstCheckbox.nativeElement.dispatchEvent(new Event('change'));

expect(onSelectionToggleSpy).toHaveBeenCalledWith('runid');
});
});
27 changes: 24 additions & 3 deletions tensorboard/webapp/runs/views/runs_table/runs_table_container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function matchFilter(
[regexFilter]="regexFilter$ | async"
[sortOption]="sortOption$ | async"
[usePagination]="usePagination"
(onSelectionToggle)="onRunSelectionToggle($event)"
(onSelectionToggle)="onRunItemSelectionToggle($event)"
(onSelectionDblClick)="onRunSelectionDblClick($event)"
(onPageSelectionToggle)="onPageSelectionToggle($event)"
(onPaginationChange)="onPaginationChange($event)"
Expand All @@ -239,6 +239,8 @@ function matchFilter(
[sortingInfo]="sortingInfo$ | async"
(sortDataBy)="sortDataBy($event)"
(orderColumns)="orderColumns($event)"
(onSelectionToggle)="onRunSelectionToggle($event)"
(onAllSelectionToggle)="onAllSelectionToggle($event)"
></runs-data-table>
`,
host: {
Expand Down Expand Up @@ -579,15 +581,18 @@ export class RunsTableContainer implements OnInit, OnDestroy {
return combineLatest([
this.store.select(getRuns, {experimentId}),
this.store.select(getRunColorMap),
this.store.select(getCurrentRouteRunSelection),
this.runsColumns$,
this.runToHParamValues$,
]).pipe(
map(([runs, colorMap, runsColumns, runToHParamValues]) => {
map(([runs, colorMap, selectionMap, runsColumns, runToHParamValues]) => {
return runs.map((run) => {
const tableData: TableData = {
id: run.id,
color: colorMap[run.id],
selected: Boolean(selectionMap?.get(run.id)),
};

runsColumns.forEach((column) => {
switch (column.type) {
case ColumnHeaderType.RUN:
Expand Down Expand Up @@ -646,14 +651,22 @@ export class RunsTableContainer implements OnInit, OnDestroy {
);
}

onRunSelectionToggle(item: RunTableItem) {
onRunItemSelectionToggle(item: RunTableItem) {
this.store.dispatch(
runSelectionToggled({
runId: item.run.id,
})
);
}

onRunSelectionToggle(id: string) {
this.store.dispatch(
runSelectionToggled({
runId: id,
})
);
}

onRunSelectionDblClick(item: RunTableItem) {
// Note that a user's double click will trigger both 'change' and 'dblclick'
// events so onRunSelectionToggle() will also be called and we will fire
Expand All @@ -674,6 +687,14 @@ export class RunsTableContainer implements OnInit, OnDestroy {
);
}

onAllSelectionToggle(runIds: string[]) {
this.store.dispatch(
runPageSelectionToggled({
runIds,
})
);
}

// When `usePagination` is false, page selection affects the single page,
// containing all items.
onPageSelectionToggle(event: {items: RunTableItem[]}) {
Expand Down
Loading