web/frontend/src/app/shared/services/tab-storage.service.ts (229 lines of code) (raw):
import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { StorageMap } from '@ngx-pwa/local-storage';
import { combineLatest, merge, Observable, of, ReplaySubject, Subject } from 'rxjs';
import {
buffer,
concatMap,
distinctUntilChanged,
filter,
last,
map, scan,
startWith,
switchMap,
take,
takeUntil,
tap,
} from 'rxjs/operators';
import { ChartTypes } from '../../pages/streams/models/chart.model';
import { TabModel } from '../../pages/streams/models/tab.model';
@Injectable({
providedIn: 'root',
})
export class TabStorageService<T> implements OnDestroy {
snapShot: Partial<T>;
private destroy$ = new ReplaySubject(1);
private syncQueue$ = new Subject<[Observable<any>, Subject<any>]>();
private dataUpdate$ = new Subject<Partial<T>>();
private additionalKey: string;
private clones: { [index: string]: TabStorageService<unknown> } = {};
private syncFlowsInTab = {rightPanel: ['showMessageInfo', 'showProps', 'showViewInfo', 'messageView', 'showChartSettings', 'showDescription']};
private syncConditions = {
showViewInfo: (tab: TabModel) => tab.isView,
showChartSettings: (tab: TabModel) => tab.filter?.chart_type === ChartTypes.LINEAR,
};
private localStorageStore = {rightPanel: ['showMessageInfo', 'showProps', 'showViewInfo', 'messageView', 'showChartSettings', 'showDescription']};
constructor(private localStorage: StorageMap, private activatedRoute: ActivatedRoute) {
this.syncQueue$
.pipe(
concatMap(([source$, resolve$]) =>
source$.pipe(
take(1),
map((result) => ({result, resolve$})),
),
),
tap(({result, resolve$}) => {
resolve$.next(result);
resolve$.complete();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.getData()
.pipe(takeUntil(this.destroy$))
.subscribe((data) => (this.snapShot = data));
}
save(data: Partial<T>, doUpdate = true): Observable<boolean> {
this.snapShot = data;
return this.tabId().pipe(
take(1),
switchMap((tabId) => this.setByKey(this.key(tabId), data)),
tap(() => {
if (doUpdate) {
this.dataUpdate$.next(data);
}
}),
);
}
updateData(callback: (data: Partial<T>) => Partial<T>, doUpdate = true): Observable<boolean> {
return this.getData().pipe(
take(1),
switchMap((data) => this.save(callback(data), doUpdate)),
);
}
updateDataSync(callback: (data: Partial<T>) => Partial<T>): void {
this.viaQueue(this.updateData(callback)).subscribe();
}
getDataSync(
keys: string[] = null,
getUpdates = true,
ignoreLocalStorage = true,
): Observable<Partial<T>> {
return this.viaQueue(this.getData(keys, getUpdates, ignoreLocalStorage));
}
getData(
keys: string[] = null,
getUpdates = true,
ignoreLocalStorage = true,
): Observable<Partial<T>> {
const data$ = this.tabId().pipe(
switchMap((tabId) => {
let getter = this.localStorage.get(this.key(tabId)) as Observable<Partial<T>>;
if (
!ignoreLocalStorage &&
this.additionalKey &&
this.localStorageStore[this.additionalKey]
) {
const localValue = localStorage.getItem(this.key(tabId));
getter = getter.pipe(startWith(localValue ? JSON.parse(localValue) : {}));
}
return merge(getter, this.dataUpdate$.pipe(filter(() => getUpdates)));
}),
);
if (keys) {
const uniqueStr = (data: Partial<T>) => {
return JSON.stringify(keys.map((key) => (data ? data[key] : null)));
};
return data$.pipe(distinctUntilChanged((p, c) => uniqueStr(p) === uniqueStr(c)));
}
return data$;
}
removeData(tabId: string): Observable<boolean> {
return this.localStorage
.keys()
.pipe(
concatMap((key) =>
key.startsWith(this.key(tabId)) ? this.deleteByKey(key) : of(undefined),
),
);
}
removeAllData(): Observable<undefined> {
return this.localStorage
.keys()
.pipe(
concatMap((key) => (key.startsWith('tabsStorage') ? this.deleteByKey(key) : of(undefined))),
);
}
replaceTab(from: TabModel, to: TabModel): Observable<boolean> {
return this.localStorage.keys().pipe(
concatMap((key) => {
const syncFlow = Object.keys(this.syncFlowsInTab).find(
(flow) => key === `tabsStorage${from.id}${flow}`,
);
if (syncFlow) {
return this.localStorage.get(key).pipe(
switchMap((data) => {
const toSync = {};
Object.keys(data)
.filter((dataKey) => this.syncFlowsInTab[syncFlow].includes(dataKey))
.forEach((dataKey) => {
if (!this.syncConditions[dataKey] || this.syncConditions[dataKey](to)) {
toSync[dataKey] = data[dataKey];
}
});
return this.setByKey(`tabsStorage${to.id}${syncFlow}`, toSync, syncFlow);
}),
concatMap(() => this.deleteByKey(key)),
);
}
return key.startsWith(`tabsStorage${from.id}`) ? this.deleteByKey(key) : of(null);
}),
last(),
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
Object.keys(this.clones).forEach(key => this.clones[key].ngOnDestroy());
}
flow<F>(key: string): TabStorageService<F> {
if (!this.clones[key]) {
this.clones[key] = new TabStorageService<F>(this.localStorage, this.activatedRoute);
this.clones[key].setAdditionalKey(`${this.additionalKey || ''}${key}`);
}
return this.clones[key];
}
allDataForTab(tabId: string, ignoreKeys: RegExp[] = []): Observable<{[index: string]: unknown}> {
const result = {};
const keys = [];
const tabKey = this.key(tabId);
return this.localStorage.keys().pipe(
tap(key => {
if (key.startsWith(tabKey)) {
const subKey = key.substr(tabKey.length);
if (!ignoreKeys.find(regExp => regExp.test(subKey))) {
keys.push(subKey);
}
}
}),
last(),
switchMap(() => keys.length ? combineLatest(keys.map(k => this.localStorage.get(`${tabKey}${k}`))) : of([])),
map((data) => {
keys.forEach((k, i) => result[k] = data[i]);
return result;
}),
);
}
setTabData(tabId: string, data: {[index: string]: unknown}): Observable<void> {
const saves = Object.keys(data || {}).map(key => this.localStorage.set(`${this.key(tabId)}${key}`, data[key]));
return saves.length ? combineLatest(saves) : of(null);
}
removeFlow(key: string): Observable<void> {
return this.tabId().pipe(concatMap(tabId => this.localStorage.delete(this.flow(key).key(tabId)).pipe(tap(() => {
delete this.clones[key];
}))));
}
private tabId(): Observable<string> {
return this.activatedRoute.params.pipe(map(({id}) => id));
}
private setAdditionalKey(key: string): void {
this.additionalKey = key;
}
private key(tabId: string): string {
return `tabsStorage${tabId}${this.additionalKey || ''}`;
}
private viaQueue(source$: Observable<any>): Observable<any> {
const subject$ = new Subject<any>();
this.syncQueue$.next([source$, subject$]);
return subject$;
}
private setByKey(key: string, data: Partial<T>, flow = null): Observable<boolean> {
const currentFlow = flow || this.additionalKey;
if (currentFlow && this.localStorageStore[currentFlow]) {
const localData = {};
this.localStorageStore[currentFlow].forEach((k) => {
if (data?.[k]) {
localData[k] = data?.[k];
}
});
localStorage.setItem(key, JSON.stringify(localData));
}
return this.localStorage.set(key, data);
}
private deleteByKey(key: string): Observable<boolean> {
if (localStorage.getItem(key)) {
localStorage.removeItem(key);
}
return this.localStorage.delete(key);
}
}