Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add column config local storage support to table #2417

Merged
merged 8 commits into from
Sep 21, 2023
18 changes: 18 additions & 0 deletions projects/common/src/preference/preference.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@
);
}

/**
* Returns the current storage value if defined, else the default value.
*/
public getOnce<T extends PreferenceValue>(
key: PreferenceKey,
defaultValue?: T,
type: StorageType = PreferenceService.DEFAULT_STORAGE_TYPE
): T {
const storedValue = this.preferenceStorage(type).get(this.asStorageKey(key));
const value = this.fromStorageValue<T>(storedValue) ?? defaultValue;

if (value === undefined) {
throw Error(`No value found or default provided for preferenceKey: ${key}`);

Check warning on line 70 in projects/common/src/preference/preference.service.ts

View check run for this annotation

Codecov / codecov/patch

projects/common/src/preference/preference.service.ts#L70

Added line #L70 was not covered by tests
}

return value;
}

public set(
key: PreferenceKey,
value: PreferenceValue,
Expand Down
9 changes: 7 additions & 2 deletions projects/components/src/table/table.component.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import { fakeAsync, flush } from '@angular/core/testing';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { DomElementMeasurerService, NavigationService } from '@hypertrace/common';
import { DomElementMeasurerService, NavigationService, PreferenceService, PreferenceValue } from '@hypertrace/common';
import { runFakeRxjs } from '@hypertrace/test-utils';
import { createHostFactory, mockProvider } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
Expand All @@ -20,6 +20,7 @@ import { TableColumnConfigExtended, TableService } from './table.service';
import { ModalService } from '../modal/modal.service';

describe('Table component', () => {
let localStorage: PreferenceValue = { columns: [] };
// TODO remove builders once table stops mutating inputs
const buildData = () => [
{
Expand Down Expand Up @@ -56,7 +57,6 @@ describe('Table component', () => {
imports: [LetAsyncModule],
providers: [
mockProvider(NavigationService),
mockProvider(ActivatedRoute),
mockProvider(ActivatedRoute, {
queryParamMap: EMPTY
}),
Expand All @@ -75,6 +75,10 @@ describe('Table component', () => {
}),
mockProvider(ModalService, {
createModal: jest.fn().mockReturnValue({ closed$: of([]) })
}),
mockProvider(PreferenceService, {
getOnce: () => localStorage,
set: (_: unknown, value: PreferenceValue) => (localStorage = value)
})
],
declarations: [MockComponent(PaginatorComponent), MockComponent(SearchBoxComponent)],
Expand Down Expand Up @@ -273,6 +277,7 @@ describe('Table component', () => {
syncWithUrl: false
}
});
spectator.tick();

spectator.component.onSortChange(TableSortDirection.Ascending, columns[0]);

Expand Down
86 changes: 75 additions & 11 deletions projects/components/src/table/table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/* eslint-disable @angular-eslint/component-max-inline-declarations */
import { ModalService } from '../modal/modal.service';
import {
TableEditColumnsModalConfig,
TableEditColumnsModalComponent
TableEditColumnsModalComponent,
TableEditColumnsModalConfig
} from './columns/table-edit-columns-modal.component';
import { CdkHeaderRow } from '@angular/cdk/table';
import {
Expand All @@ -29,13 +29,16 @@
Dictionary,
DomElementMeasurerService,
isEqualIgnoreFunctions,
isNonEmptyString,
NavigationService,
NumberCoercer,
PreferenceService,
StorageType,
TypedSimpleChanges
} from '@hypertrace/common';
import { isNil, without } from 'lodash-es';
import { isNil, pick, without } from 'lodash-es';
import { BehaviorSubject, combineLatest, merge, Observable, Subject } from 'rxjs';
import { switchMap, take, filter, map } from 'rxjs/operators';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { FilterAttribute } from '../filtering/filter/filter-attribute';
import { LoadAsyncConfig } from '../load-async/load-async.service';
import { PageEvent } from '../paginator/page.event';
Expand Down Expand Up @@ -308,6 +311,9 @@
id: '$$detail'
};

@Input()
public id?: string;

@Input()
public columnConfigs?: TableColumnConfig[];

Expand Down Expand Up @@ -433,7 +439,9 @@
private readonly columnConfigsSubject: BehaviorSubject<TableColumnConfigExtended[]> = new BehaviorSubject<
TableColumnConfigExtended[]
>([]);
public readonly columnConfigs$: Observable<TableColumnConfigExtended[]> = this.columnConfigsSubject.asObservable();
public readonly columnConfigs$: Observable<
TableColumnConfigExtended[]
> = this.columnConfigsSubject.asObservable().pipe(tap(columnConfigs => this.saveTablePreferences(columnConfigs)));
public columnDefaultConfigs?: TableColumnConfigExtended[];

public visibleColumnConfigs: TableColumnConfigExtended[] = [];
Expand Down Expand Up @@ -505,7 +513,8 @@
private readonly activatedRoute: ActivatedRoute,
private readonly domElementMeasurerService: DomElementMeasurerService,
private readonly tableService: TableService,
private readonly modalService: ModalService
private readonly modalService: ModalService,
private readonly preferenceService: PreferenceService
) {
combineLatest([this.activatedRoute.queryParamMap, this.columnConfigs$])
.pipe(
Expand Down Expand Up @@ -686,6 +695,49 @@
}
}

private dehydratePersistedColumnConfig(column: TableColumnConfig): PersistedTableColumnConfig {
/*
* Note: The table columns have nested methods, so those are lost here when persistService uses JSON.stringify
* to convert and store. We want to just pluck the relevant properties that are required to be saved.
*/
return pick(column, ['id', 'visible']);
}

private saveTablePreferences(columns: TableColumnConfig[]): void {
if (isNonEmptyString(this.id)) {
this.setLocalPreferences({
...this.getLocalPreferences(),
columns: columns.map(column => this.dehydratePersistedColumnConfig(column))
});
}
}

private getLocalPreferences(): TableLocalPreferences {
return isNonEmptyString(this.id)
? this.preferenceService.getOnce<TableLocalPreferences>(this.id, { columns: [] }, StorageType.Local)
: { columns: [] };
}

private setLocalPreferences(preferences: TableLocalPreferences): void {
if (isNonEmptyString(this.id)) {
this.preferenceService.set(this.id, preferences, StorageType.Local);
}
}

private hydratePersistedColumnConfigs(
columns: TableColumnConfigExtended[],
persistedColumns: PersistedTableColumnConfig[]
): TableColumnConfigExtended[] {
return columns.map(column => {
const found = persistedColumns.find(persistedColumn => persistedColumn.id === column.id);

return {
...column, // Apply default column config
...(found ? found : {}) // Override with any saved properties
};
});
}

private setColumnResizeDefaults(columnResizeHandler: HTMLDivElement): void {
columnResizeHandler.style.backgroundColor = Color.Transparent;
columnResizeHandler.style.right = '2px';
Expand Down Expand Up @@ -714,15 +766,20 @@
this.checkColumnWidthCompatibilityOrThrow(column.width);
this.checkColumnWidthCompatibilityOrThrow(column.minWidth);
});
const columnConfigurations = this.buildColumnConfigExtendeds(columnConfigs ?? this.columnConfigs ?? []);
const columnConfigurations = this.buildColumnConfigExtended(columnConfigs ?? this.columnConfigs ?? []);
if (isNil(this.columnDefaultConfigs)) {
this.columnDefaultConfigs = columnConfigurations;
}
const visibleColumns = columnConfigurations.filter(column => column.visible);

const preferences = this.getLocalPreferences();
const columnConfigsWithPersistedData = this.hydratePersistedColumnConfigs(
columnConfigurations,
preferences.columns ?? []
);
const visibleColumns = columnConfigsWithPersistedData.filter(column => column.visible);
this.initialColumnConfigIdWidthMap = new Map(visibleColumns.map(column => [column.id, column.width ?? -1]));
this.updateVisibleColumns(visibleColumns);

this.columnConfigsSubject.next(columnConfigurations);
this.columnConfigsSubject.next(columnConfigsWithPersistedData);
}

private checkColumnWidthCompatibilityOrThrow(width?: TableColumnWidth): void {
Expand Down Expand Up @@ -849,6 +906,7 @@
)
)
.subscribe(editedColumnConfigs => {
this.saveTablePreferences(editedColumnConfigs);

Check warning on line 909 in projects/components/src/table/table.component.ts

View check run for this annotation

Codecov / codecov/patch

projects/components/src/table/table.component.ts#L909

Added line #L909 was not covered by tests
this.initializeColumns(editedColumnConfigs);
});
}
Expand Down Expand Up @@ -913,7 +971,7 @@
return this.hasExpandableRows() ? index - 1 : index;
}

private buildColumnConfigExtendeds(columnConfigs: TableColumnConfig[]): TableColumnConfigExtended[] {
private buildColumnConfigExtended(columnConfigs: TableColumnConfig[]): TableColumnConfigExtended[] {
const stateColumns = [];

if (this.hasMultiSelect()) {
Expand Down Expand Up @@ -1183,3 +1241,9 @@
element: HTMLElement;
bounds: ColumnBounds;
}

interface TableLocalPreferences {
columns?: PersistedTableColumnConfig[];
}

type PersistedTableColumnConfig = Pick<TableColumnConfig, 'id' | 'visible'>;
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import {
} from '@hypertrace/components';
import { WidgetRenderer } from '@hypertrace/dashboards';
import { Renderer } from '@hypertrace/hyperdash';
import { RendererApi, RENDERER_API } from '@hypertrace/hyperdash-angular';
import { capitalize, isEmpty, isEqual, pick } from 'lodash-es';
import { RENDERER_API, RendererApi } from '@hypertrace/hyperdash-angular';
import { capitalize, isEmpty, isEqual } from 'lodash-es';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import {
filter,
Expand All @@ -58,7 +58,6 @@ import { MetadataService } from '../../../services/metadata/metadata.service';
import { InteractionHandler } from '../../interaction/interaction-handler';
import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model';
import { TableWidgetBaseModel } from './table-widget-base.model';
import { SpecificationBackedTableColumnDef } from './table-widget-column.model';
import { TableWidgetControlSelectOptionModel } from './table-widget-control-select-option.model';
import { TableWidgetViewToggleModel } from './table-widget-view-toggle.model';
import { TableWidgetModel } from './table-widget.model';
Expand Down Expand Up @@ -96,6 +95,7 @@ import { TableWidgetModel } from './table-widget.model';

<ht-table
class="table"
[id]="this.model.getId()"
[columnConfigs]="this.columnConfigs$ | async"
[metadata]="this.metadata$ | async"
[mode]="this.model.mode"
Expand All @@ -113,7 +113,6 @@ import { TableWidgetModel } from './table-widget.model';
[maxRowHeight]="this.api.model.getMaxRowHeight()"
(rowClicked)="this.onRowClicked($event)"
(selectionsChange)="this.onRowSelection($event)"
(columnConfigsChange)="this.onColumnsChange($event)"
(visibleColumnsChange)="this.onVisibleColumnsChange($event)"
>
</ht-table>
Expand Down Expand Up @@ -316,21 +315,14 @@ export class TableWidgetRendererComponent
}

private getColumnConfigs(): Observable<TableColumnConfig[]> {
return this.getLocalPreferences().pipe(
switchMap(preferences =>
combineLatest([this.getScope(), this.api.change$.pipe(mapTo(true), startWith(true))]).pipe(
switchMap(([scope]) => this.model.getColumns(scope)),
startWith([]),
map((columns: SpecificationBackedTableColumnDef[]) =>
this.hydratePersistedColumnConfigs(columns, preferences.columns ?? [])
),
pairwise(),
filter(([previous, current]) => !isEqualIgnoreFunctions(previous, current)),
map(([_, current]) => current),
share(),
tap(() => this.onDashboardRefresh())
)
)
return combineLatest([this.getScope(), this.api.change$.pipe(mapTo(true), startWith(true))]).pipe(
switchMap(([scope]) => this.model.getColumns(scope)),
startWith([]),
pairwise(),
filter(([previous, current]) => !isEqualIgnoreFunctions(previous, current)),
map(([_, current]) => current),
share(),
tap(() => this.onDashboardRefresh())
);
}

Expand Down Expand Up @@ -512,17 +504,6 @@ export class TableWidgetRendererComponent
});
}

public onColumnsChange(columns: TableColumnConfig[]): void {
if (isNonEmptyString(this.model.getId())) {
this.getLocalPreferences().subscribe(preferences =>
this.setLocalPreferences({
...preferences,
columns: columns.map(column => this.dehydratePersistedColumnConfig(column))
})
);
}
}

public onRowClicked(row: StatefulTableRow): void {
this.getRowClickInteractionHandler(row)?.execute(row);
}
Expand Down Expand Up @@ -558,28 +539,6 @@ export class TableWidgetRendererComponent
: viewItems[TableWidgetRendererComponent.DEFAULT_TAB_INDEX];
}

private hydratePersistedColumnConfigs(
columns: SpecificationBackedTableColumnDef[],
persistedColumns: TableColumnConfig[]
): SpecificationBackedTableColumnDef[] {
return columns.map(column => {
const found = persistedColumns.find(persistedColumn => persistedColumn.id === column.id);

return {
...column, // Apply default column config
...(found ? found : {}) // Override with any saved properties
};
});
}

private dehydratePersistedColumnConfig(column: TableColumnConfig): PersistedTableColumnConfig {
/*
* Note: The table columns have nested methods, so those are lost here when persistService uses JSON.stringify
* to convert and store. We want to just pluck the relevant properties that are required to be saved.
*/
return pick(column, ['id', 'visible']);
}

private getViewPreferences(): Observable<TableWidgetViewPreferences> {
return isNonEmptyString(this.model.viewId)
? this.preferenceService.get<TableWidgetViewPreferences>(this.model.viewId, {}, StorageType.Local).pipe(first())
Expand All @@ -592,20 +551,6 @@ export class TableWidgetRendererComponent
}
}

private getLocalPreferences(): Observable<TableWidgetLocalPreferences> {
return isNonEmptyString(this.model.getId())
? this.preferenceService
.get<TableWidgetLocalPreferences>(this.model.getId()!, {}, StorageType.Local)
.pipe(first())
: of({});
}

private setLocalPreferences(preferences: TableWidgetLocalPreferences): void {
if (isNonEmptyString(this.model.getId())) {
this.preferenceService.set(this.model.getId()!, preferences, StorageType.Local);
}
}

private getSessionPreferences(): Observable<TableWidgetSessionPreferences> {
return isNonEmptyString(this.model.getId())
? this.preferenceService
Expand Down Expand Up @@ -649,13 +594,7 @@ interface TableWidgetViewPreferences {
activeView?: string;
}

interface TableWidgetLocalPreferences {
columns?: PersistedTableColumnConfig[];
}

interface TableWidgetSessionPreferences {
checkboxes?: TableCheckboxControl[];
selections?: TableSelectControl[];
}

type PersistedTableColumnConfig = Pick<TableColumnConfig, 'id' | 'visible'>;
Loading