Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
40 changes: 35 additions & 5 deletions projects/common/src/preference/preference.service.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AbstractStorage } from '../utilities/browser/storage/abstract-storage';
import { LocalStorage } from '../utilities/browser/storage/local-storage';
import { SessionStorage } from '../utilities/browser/storage/session-storage';
import { BooleanCoercer } from '../utilities/coercers/boolean-coercer';
import { NumberCoercer } from '../utilities/coercers/number-coercer';

export const enum StorageType {
Local = 'local',
Session = 'session'
}

@Injectable({
providedIn: 'root'
})
export class PreferenceService {
private static readonly DEFAULT_STORAGE_TYPE: StorageType = StorageType.Local;

private static readonly PREFERENCE_STORAGE_NAMESPACE: string = 'preference';
private static readonly SEPARATOR_CHAR: string = '.';
private static readonly SEPARATOR_REGEX: RegExp = /\.(.+)/;
private readonly numberCoercer: NumberCoercer = new NumberCoercer();
private readonly booleanCoercer: BooleanCoercer = new BooleanCoercer();

public constructor(private readonly preferenceStorage: LocalStorage) {}
public constructor(
private readonly localStorage: LocalStorage,
private readonly sessionStorage: SessionStorage
) {}

/**
* Returns the current storage value if defined, else the default value. The observable
* will continue to emit as the preference is updated, reverting to default value if the
* preference becomes unset. If default value is not provided, the observable will
* throw in the case the preference is unset.
*/
public get<T extends PreferenceValue>(key: PreferenceKey, defaultValue?: T): Observable<T> {
return this.preferenceStorage.watch(this.asStorageKey(key)).pipe(
public get<T extends PreferenceValue>(
key: PreferenceKey,
defaultValue?: T,
type: StorageType = PreferenceService.DEFAULT_STORAGE_TYPE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think I'd argue that get should not take a type argument. We should read this from wherever it is written, first looking at session storage, then local storage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I agree. That seems like it could cause unexpected behavior. Prefer to hold off on that idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can hold off for now and continue the discussion async, but it allows the writer and reader to decouple. The attribute is just as valid in either location, so the reader shouldn't need to care.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a fair argument. My problem with this implementation is exactly that, so your suggestion does seem like a reasonable trade-off. We'll revisit.

): Observable<T> {
return this.preferenceStorage(type).watch(this.asStorageKey(key)).pipe(
map(storedValue => this.fromStorageValue<T>(storedValue) ?? defaultValue),
switchMap(value =>
value === undefined
Expand All @@ -34,9 +50,13 @@ export class PreferenceService {
);
}

public set(key: PreferenceKey, value: PreferenceValue): void {
public set(
key: PreferenceKey,
value: PreferenceValue,
type: StorageType = PreferenceService.DEFAULT_STORAGE_TYPE
): void {
const val = this.asStorageValue(value);
this.preferenceStorage.set(this.asStorageKey(key), val);
this.preferenceStorage(type).set(this.asStorageKey(key), val);
}

private asStorageKey(key: PreferenceKey): PreferenceStorageKey {
Expand Down Expand Up @@ -70,6 +90,16 @@ export class PreferenceService {
return undefined;
}
}

private preferenceStorage(type: StorageType): AbstractStorage {
switch (type) {
case StorageType.Session:
return this.sessionStorage;
case StorageType.Local:
default:
return this.localStorage;
}
}
}

type PreferenceStorageKey = string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
import { AbstractStorage } from './abstract-storage';

@Injectable({ providedIn: 'root' })
export class SessionStorage extends AbstractStorage {
public constructor() {
super(sessionStorage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
QueryList
} from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { queryListAndChanges$, SubscriptionLifecycle } from '@hypertrace/common';
import { queryListAndChanges$ } from '@hypertrace/common';
import { BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { ButtonRole, ButtonStyle } from '../button/button';
Expand All @@ -24,7 +24,6 @@ import { MultiSelectJustify } from './multi-select-justify';
selector: 'ht-multi-select',
styleUrls: ['./multi-select.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [SubscriptionLifecycle],
template: `
<div
class="multi-select"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
forkJoinSafeEmpty,
isEqualIgnoreFunctions,
isNonEmptyString,
PreferenceService
PreferenceService,
StorageType
} from '@hypertrace/common';
import {
FilterAttribute,
Expand All @@ -31,7 +32,7 @@ import {
} from '@hypertrace/components';
import { WidgetRenderer } from '@hypertrace/dashboards';
import { Renderer } from '@hypertrace/hyperdash';
import { RendererApi, RENDERER_API } from '@hypertrace/hyperdash-angular';
import { RENDERER_API, RendererApi } from '@hypertrace/hyperdash-angular';
import { capitalize, isEmpty, isEqual, pick } from 'lodash-es';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import {
Expand Down Expand Up @@ -548,27 +549,27 @@ export class TableWidgetRendererComponent

private getViewPreferences(): Observable<TableWidgetViewPreferences> {
return isNonEmptyString(this.model.viewId)
? this.preferenceService.get<TableWidgetViewPreferences>(this.model.viewId, {}).pipe(first())
? this.preferenceService.get<TableWidgetViewPreferences>(this.model.viewId, {}, StorageType.Session).pipe(first())
: of({});
}

private setViewPreferences(preferences: TableWidgetViewPreferences): void {
if (isNonEmptyString(this.model.viewId)) {
this.preferenceService.set(this.model.viewId, preferences);
this.preferenceService.set(this.model.viewId, preferences, StorageType.Session);
}
}

private getPreferences(
defaultPreferences: TableWidgetPreferences = TableWidgetRendererComponent.DEFAULT_PREFERENCES
): Observable<TableWidgetPreferences> {
return isNonEmptyString(this.model.getId())
? this.preferenceService.get<TableWidgetPreferences>(this.model.getId()!, defaultPreferences).pipe(first())
? this.preferenceService.get<TableWidgetPreferences>(this.model.getId()!, defaultPreferences, StorageType.Session).pipe(first())
: of(defaultPreferences);
}

private setPreferences(preferences: TableWidgetPreferences): void {
if (isNonEmptyString(this.model.getId())) {
this.preferenceService.set(this.model.getId()!, preferences);
this.preferenceService.set(this.model.getId()!, preferences, StorageType.Session);
}
}

Expand Down