diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package-lock.json b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package-lock.json
index 2b518f00..9767f1a5 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package-lock.json
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package-lock.json
@@ -3304,6 +3304,21 @@
"keyv": "*"
}
},
+ "@types/lodash": {
+ "version": "4.14.184",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.184.tgz",
+ "integrity": "sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==",
+ "dev": true
+ },
+ "@types/lodash-es": {
+ "version": "4.17.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
+ "integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"@types/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package.json b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package.json
index d68724e7..bd8954b5 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package.json
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/package.json
@@ -54,6 +54,7 @@
"@kubernetes/client-node": "^0.16.3",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "^2.0.9",
+ "@types/lodash-es": "^4.17.6",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "4.28.2",
"@typescript-eslint/parser": "4.28.2",
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.html b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.html
index 0cc4e395..ae4b3961 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.html
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.html
@@ -33,40 +33,44 @@
- Status
- Filter based on the objects' status, by using:
-
- - The value shown in the tooltip
- -
- The status types (i.e.
- ready, waiting, warning, error, unavailable, uninitialized,
- terminating, stopped)
-
-
+
+ Status
+ Filter based on the objects' status, by using:
+
+ - The value shown in the tooltip
+ -
+ The status types (i.e.
+ ready, waiting, warning, error, unavailable, uninitialized,
+ terminating, stopped)
+
+
+
- Date
- Filter timestamp values based on:
-
- -
- The "-" character for empty date values:
-
- Created at: -
-
-
-
-
- -
- The relative time text, shown in the column (i.e. X months ago)
-
- -
- The substring of the timestamp, that is shown in the UTC part of the
- tooltip
-
-
+
+ Date
+ Filter timestamp values based on:
+
+ -
+ The "-" character for empty date values:
+
+ Created at: -
+
+
+
+
+ -
+ The relative time text, shown in the column (i.e. X months ago)
+
+ -
+ The substring of the timestamp, that is shown in the UTC part of the
+ tooltip
+
+
+
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.ts b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.ts
index e4c3fa92..c45293b0 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.ts
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/help-popover/help-popover.component.ts
@@ -8,6 +8,12 @@ import { Component, OnInit, Input } from '@angular/core';
export class HelpPopoverComponent implements OnInit {
@Input() popoverPosition = 'below';
+ @Input()
+ showStatus: boolean;
+
+ @Input()
+ showDate: boolean;
+
constructor() {}
ngOnInit(): void {}
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html
index f8a041f0..c02422f8 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html
@@ -41,17 +41,15 @@
aria-label="Help"
(click)="$event.stopPropagation()"
>
-
+
+
-
+
{{ header.title }}
@@ -249,7 +247,13 @@
-
+
+ {{row}}
+
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.scss b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.scss
index 7c8a2062..6c11ce34 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.scss
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.scss
@@ -72,3 +72,7 @@ lib-action {
.filter-header {
margin-right: 8px;
}
+
+.highlight-row {
+ background-color: #ffffcd;
+}
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.spec.ts b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.spec.ts
index a2fc30e2..b88ff9f0 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.spec.ts
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.spec.ts
@@ -2,8 +2,93 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TableComponent } from './table.component';
import { ResourceTableModule } from '../resource-table.module';
-import { PropertyValue, DateTimeValue } from '../types';
import { quantityToScalar } from '@kubernetes/client-node/dist/util';
+import {
+ PropertyValue,
+ DateTimeValue,
+ StatusValue,
+ ComponentValue,
+} from '../types';
+import { MatChipInputEvent } from '@angular/material/chips';
+import { TableColumnComponent } from '../component-value/component-value.component';
+import { Component, SimpleChange } from '@angular/core';
+import subMonths from 'date-fns/sub_months';
+import { cloneDeep } from 'lodash-es';
+
+@Component({
+ selector: 'lib-server-type',
+ template: ``,
+})
+export class ServerTypeComponent implements TableColumnComponent {
+ constructor() {}
+
+ notebookServerType: string;
+
+ set element(notebook) {
+ this.notebookServerType = notebook.serverType;
+ }
+}
+
+const tableConfig = {
+ title: 'test',
+ columns: [
+ {
+ matHeaderCellDef: `Status`,
+ matColumnDef: 'status',
+ value: new StatusValue(),
+ },
+ {
+ matHeaderCellDef: `Name`,
+ matColumnDef: 'name',
+ value: new PropertyValue({
+ field: 'name',
+ tooltipField: 'name',
+ truncate: true,
+ }),
+ },
+ {
+ matHeaderCellDef: `Type`,
+ matColumnDef: 'type',
+ value: new ComponentValue({
+ component: ServerTypeComponent,
+ }),
+ filteringPreprocessorFn: element => element.serverType,
+ },
+ {
+ matHeaderCellDef: `Created at`,
+ matColumnDef: 'age',
+ value: new DateTimeValue({ field: 'age' }),
+ },
+ ],
+};
+
+const tableData = [
+ {
+ status: {
+ phase: 'ready',
+ message: 'Running',
+ },
+ name: 'a-notebook',
+ serverType: 'jupyter',
+ age: '2022-02-25T16:57:23Z',
+ },
+ {
+ status: {
+ phase: 'stopped',
+ message: 'No Pods are currently running for this Notebook Server.',
+ },
+ name: 'b-notebook',
+ serverType: 'group-one',
+ age: '2022-01-23T14:51:29Z',
+ },
+];
+
+function checkCell(compiled) {
+ const nameCell = compiled.querySelector(
+ '[data-cy-resource-table-row="Name"]',
+ );
+ expect(nameCell.textContent.replace(/\s+/g, '')).toBe('b-notebook');
+}
describe('TableComponent', () => {
let component: TableComponent;
@@ -129,4 +214,154 @@ describe('TableComponent', () => {
// your assertions, i.e. expect to see the first element being sorted in the table
expect(columnCells[0].textContent.replace(/\s+/g, '')).toBe('1');
});
+
+ it('should filter property values based on all columns', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'b-not',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter property values based on one column', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Name: b-not',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter date values based on one column using UTC timestamp', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Created at: 2022-01-23',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter date values based on one column using X months ago', () => {
+ component.config = tableConfig;
+ const tableDataCopy = cloneDeep(tableData);
+ tableDataCopy[1].age = subMonths(new Date(), 2).toISOString();
+ component.data = tableDataCopy;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Created at: 2 months ago',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter component values based on one column', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Type: group',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter status values based on one column using status phase', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Status: stopped',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should filter status values based on one column using status message', () => {
+ component.config = tableConfig;
+ component.data = tableData;
+
+ const compiled = fixture.debugElement.nativeElement;
+ const inputElement = compiled.querySelector('#filterInput');
+ component.add({
+ input: inputElement,
+ value: 'Status: No Pods',
+ } as MatChipInputEvent);
+
+ fixture.detectChanges();
+
+ checkCell(compiled);
+ });
+
+ it('should properly configure filter section', () => {
+ component.config = tableConfig;
+
+ expect(component.filteredHeaders).toEqual([]);
+ expect(component.showStatus).toEqual(false);
+ expect(component.showDate).toEqual(false);
+
+ fixture.detectChanges();
+ component.ngOnChanges({
+ config: new SimpleChange(null, component.config, true),
+ });
+
+ expect(component.filteredHeaders).toEqual([
+ { title: 'Status' },
+ { title: 'Name' },
+ { title: 'Type' },
+ { title: 'Created at' },
+ ]);
+ expect(component.showStatus).toEqual(true);
+ expect(component.showDate).toEqual(true);
+
+ const configCopy = cloneDeep(tableConfig);
+ configCopy.columns.pop();
+ component.ngOnChanges({
+ config: new SimpleChange(null, configCopy, false),
+ });
+
+ expect(component.filteredHeaders).toEqual([
+ { title: 'Status' },
+ { title: 'Name' },
+ { title: 'Type' },
+ ]);
+ expect(component.showStatus).toEqual(true);
+ expect(component.showDate).toEqual(false);
+ });
});
diff --git a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts
index 2f88e573..d9a3996e 100644
--- a/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts
+++ b/notebooks/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts
@@ -9,6 +9,8 @@ import {
OnDestroy,
OnInit,
ElementRef,
+ SimpleChanges,
+ OnChanges,
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
@@ -28,24 +30,26 @@ import {
import { DateTimeValue } from '../types/date-time';
import { TemplateValue } from '../types/template';
import { NamespaceService } from '../../services/namespace.service';
-import { Observable, Subscription } from 'rxjs';
+import { Subscription } from 'rxjs';
import { addColumn, NAMESPACE_COLUMN, removeColumn } from './utils';
import { MatSort } from '@angular/material/sort';
import { MatChipInputEvent } from '@angular/material/chips';
import { FormControl } from '@angular/forms';
-import { map, startWith } from 'rxjs/operators';
import {
MatAutocompleteSelectedEvent,
MatAutocompleteTrigger,
} from '@angular/material/autocomplete';
import { DateTimeService } from '../../services/date-time.service';
+import { isEqual } from 'lodash-es';
@Component({
selector: 'lib-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
})
-export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
+export class TableComponent
+ implements AfterViewInit, OnInit, OnDestroy, OnChanges
+{
private nsSub = new Subscription();
private innerData: any[] = [];
public dataSource = new MatTableDataSource();
@@ -64,10 +68,12 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
chipList = [];
chips = [];
- headers = [];
+ headers: { title: string }[] = [];
isClear: boolean;
- filteredHeaders: Observable;
+ filteredHeaders: { title: string }[] = [];
chipCtrl = new FormControl();
+ showDate = false;
+ showStatus = false;
@HostBinding('class.lib-table') selfClass = true;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@@ -91,18 +97,18 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
this.dataSource.data = newData;
}
+ @Input()
+ highlightedRow: unknown = {};
+
// Whenever a button in a row is pressed the component will emit an event
// with information regarding the button that was pressed as well as the
// row's object.
@Output() actionsEmitter = new EventEmitter();
constructor(public ns: NamespaceService, private dtService: DateTimeService) {
- this.filteredHeaders = this.chipCtrl.valueChanges.pipe(
- startWith(null),
- map((chip: string | null) =>
- chip ? this.filter(chip) : this.headers.slice(),
- ),
- );
+ this.chipCtrl.valueChanges.subscribe(chip => {
+ this.filteredHeaders = this.filter(chip);
+ });
}
ngOnInit() {
@@ -123,8 +129,19 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
});
this.sort.sort({ disableClear: true, id: 'name', start: 'asc' });
+ }
- this.config.columns.forEach(column => {
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.config) {
+ this.configureFilter(changes.config.currentValue);
+ }
+ }
+
+ configureFilter(config: TableConfig): void {
+ this.headers = [];
+ this.showStatus = false;
+ this.showDate = false;
+ config.columns.forEach(column => {
if (
!this.isMenuValue(column.value) &&
!this.isActionListValue(column.value) &&
@@ -137,7 +154,16 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
title: column.matHeaderCellDef,
});
}
+
+ if (this.isStatusValue(column.value)) {
+ this.showStatus = true;
+ }
+
+ if (this.isDateTimeValue(column.value)) {
+ this.showDate = true;
+ }
});
+ this.filteredHeaders = this.filter(this.chipCtrl.value);
}
ngOnDestroy() {
@@ -463,7 +489,10 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
}
}
- private filter(value: string): string[] {
+ private filter(value: string | null): { title: string }[] {
+ if (value === null) {
+ return this.headers.slice();
+ }
const filterValue = value.toLowerCase();
return this.headers.filter(chip =>
@@ -471,6 +500,15 @@ export class TableComponent implements AfterViewInit, OnInit, OnDestroy {
);
}
+ highlightRow(row: unknown, highlightedRow: unknown): string {
+ try {
+ return isEqual(row, highlightedRow) ? 'highlight-row' : '';
+ } catch (error) {
+ console.error(error);
+ return '';
+ }
+ }
+
public isActionListValue(obj) {
return obj instanceof ActionListValue;
}