diff --git a/ui-ngx/src/app/core/auth/auth.effects.ts b/ui-ngx/src/app/core/auth/auth.effects.ts index 0805ff70f7..d2ebe2b4c5 100644 --- a/ui-ngx/src/app/core/auth/auth.effects.ts +++ b/ui-ngx/src/app/core/auth/auth.effects.ts @@ -18,7 +18,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { UserPreferencesService } from '@core/http/user-preferences.service'; +import { UserSettingsService } from '@core/http/user-settings.service'; import { mergeMap, withLatestFrom } from 'rxjs/operators'; import { AuthActions, AuthActionTypes } from '@core/auth/auth.actions'; import { selectAuthState } from '@core/auth/auth.selectors'; @@ -28,15 +28,15 @@ export class AuthEffects { constructor( private actions$: Actions, private store: Store, - private userPreferencesService: UserPreferencesService + private userSettingsService: UserSettingsService ) { } - persistUserPreferences = createEffect(() => this.actions$.pipe( + persistOpenedMenuSections = createEffect(() => this.actions$.pipe( ofType( AuthActionTypes.UPDATE_OPENED_MENU_SECTION, ), withLatestFrom(this.store.pipe(select(selectAuthState))), - mergeMap(([action, state]) => this.userPreferencesService.saveUserPreferences(state.authUser, state.userPreferences)) + mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections })) ), {dispatch: false}); } diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index b1a153e713..8fd1c214b5 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -15,7 +15,7 @@ /// import { AuthUser, User } from '@shared/models/user.model'; -import { UserPreferences } from '@shared/models/user-preferences.models'; +import { UserSettings } from '@shared/models/user-settings.models'; export interface SysParamsState { userTokenAccessEnabled: boolean; @@ -23,7 +23,7 @@ export interface SysParamsState { edgesSupportEnabled: boolean; hasRepository: boolean; tbelEnabled: boolean; - userPreferences: UserPreferences; + userSettings: UserSettings; } export interface AuthPayload extends SysParamsState { diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 2c0de40d7d..51786543e6 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -16,7 +16,7 @@ import { AuthPayload, AuthState } from './auth.models'; import { AuthActions, AuthActionTypes } from './auth.actions'; -import { initialUserPreferences } from '@shared/models/user-preferences.models'; +import { initialUserSettings } from '@shared/models/user-settings.models'; const emptyUserAuthState: AuthPayload = { authUser: null, @@ -27,7 +27,7 @@ const emptyUserAuthState: AuthPayload = { edgesSupportEnabled: false, hasRepository: false, tbelEnabled: false, - userPreferences: initialUserPreferences + userSettings: initialUserSettings }; export const initialState: AuthState = { @@ -62,7 +62,7 @@ export const authReducer = ( return { ...state, ...action.payload}; case AuthActionTypes.UPDATE_OPENED_MENU_SECTION: - const openedMenuSections = new Set(state.userPreferences.openedMenuSections); + const openedMenuSections = new Set(state.userSettings.openedMenuSections); if (action.payload.opened) { if (!openedMenuSections.has(action.payload.path)) { openedMenuSections.add(action.payload.path); @@ -70,8 +70,8 @@ export const authReducer = ( } else { openedMenuSections.delete(action.payload.path); } - const userPreferences = {...state.userPreferences, ...{ openedMenuSections: Array.from(openedMenuSections)}}; - return { ...state, ...{ userPreferences }}; + const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}}; + return { ...state, ...{ userSettings }}; default: return state; diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts index 3d6b3e43a7..45162565d9 100644 --- a/ui-ngx/src/app/core/auth/auth.selectors.ts +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -20,7 +20,7 @@ import { AppState } from '../core.state'; import { AuthState } from './auth.models'; import { take } from 'rxjs/operators'; import { AuthUser } from '@shared/models/user.model'; -import { UserPreferences } from '@shared/models/user-preferences.models'; +import { UserSettings } from '@shared/models/user-settings.models'; export const selectAuthState = createFeatureSelector< AuthState>( 'auth' @@ -66,14 +66,14 @@ export const selectTbelEnabled = createSelector( (state: AuthState) => state.tbelEnabled ); -export const selectUserPreferences = createSelector( +export const selectUserSettings = createSelector( selectAuthState, - (state: AuthState) => state.userPreferences + (state: AuthState) => state.userSettings ); export const selectOpenedMenuSections = createSelector( selectAuthState, - (state: AuthState) => state.userPreferences.openedMenuSections + (state: AuthState) => state.userSettings.openedMenuSections ); @@ -93,12 +93,12 @@ export const getCurrentAuthUser = (store: Store): AuthUser => { return authUser; }; -export const getCurrentUserPreferences = (store: Store): UserPreferences => { - let userPreferences: UserPreferences; - store.pipe(select(selectUserPreferences), take(1)).subscribe( - val => userPreferences = val +export const getCurrentUserSettings = (store: Store): UserSettings => { + let userSettings: UserSettings; + store.pipe(select(selectUserSettings), take(1)).subscribe( + val => userSettings = val ); - return userPreferences; + return userSettings; }; export const getCurrentOpenedMenuSections = (store: Store): string[] => { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 88c3803142..311de3d7a0 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -48,8 +48,8 @@ import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; import { isMobileApp } from '@core/utils'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { UserPasswordPolicy } from '@shared/models/settings.models'; -import { UserPreferences } from '@shared/models/user-preferences.models'; -import { UserPreferencesService } from '@core/http/user-preferences.service'; +import { UserSettings } from '@shared/models/user-settings.models'; +import { UserSettingsService } from '@core/http/user-settings.service'; @Injectable({ providedIn: 'root' @@ -68,7 +68,7 @@ export class AuthService { private dashboardService: DashboardService, private adminService: AdminService, private translate: TranslateService, - private userPreferencesService: UserPreferencesService, + private userSettingsService: UserSettingsService, private dialog: MatDialog ) { } @@ -130,7 +130,8 @@ export class AuthService { } public checkTwoFaVerificationCode(providerType: TwoFactorAuthProviderType, verificationCode: number): Observable { - return this.http.post(`/api/auth/2fa/verification/check?providerType=${providerType}&verificationCode=${verificationCode}`, + return this.http.post + (`/api/auth/2fa/verification/check?providerType=${providerType}&verificationCode=${verificationCode}`, null, defaultHttpOptions(false, true)).pipe( tap((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); @@ -493,8 +494,8 @@ export class AuthService { } } - private loadUserPreferences(authUser: AuthUser): Observable { - return this.userPreferencesService.loadUserPreferences(authUser); + private loadUserSettings(): Observable { + return this.userSettingsService.loadUserSettings(); } private loadSystemParams(authPayload: AuthPayload): Observable { @@ -503,7 +504,7 @@ export class AuthService { this.loadIsEdgesSupportEnabled(), this.loadHasRepository(authPayload.authUser), this.loadTbelEnabled(authPayload.authUser), - this.loadUserPreferences(authPayload.authUser), + this.loadUserSettings(), this.timeService.loadMaxDatapointsLimit()]; return forkJoin(sources) .pipe(map((data) => { @@ -512,8 +513,8 @@ export class AuthService { const edgesSupportEnabled: boolean = data[2] as boolean; const hasRepository: boolean = data[3] as boolean; const tbelEnabled: boolean = data[4] as boolean; - const userPreferences = data[5] as UserPreferences; - return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userPreferences}; + const userSettings = data[5] as UserSettings; + return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userSettings}; }, catchError((err) => of({})))); } diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 3dee015d56..3de5bc10e0 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -39,5 +39,5 @@ export * from './tenant.service'; export * from './tenant-profile.service'; export * from './ui-settings.service'; export * from './user.service'; -export * from './user-preferences.service'; +export * from './user-settings.service'; export * from './widget.service'; diff --git a/ui-ngx/src/app/core/http/user-preferences.service.ts b/ui-ngx/src/app/core/http/user-preferences.service.ts deleted file mode 100644 index 30f5b2e6c8..0000000000 --- a/ui-ngx/src/app/core/http/user-preferences.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -/// -/// Copyright © 2016-2023 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Injectable } from '@angular/core'; -import { AttributeService } from '@core/http/attribute.service'; -import { Observable } from 'rxjs'; -import { initialUserPreferences, UserPreferences } from '@shared/models/user-preferences.models'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { EntityType } from '@shared/models/entity-type.models'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; -import { map } from 'rxjs/operators'; -import { AuthUser } from '@shared/models/user.model'; - -const USER_PREFERENCES_ATTRIBUTE_KEY = 'user_preferences'; - -@Injectable({ - providedIn: 'root' -}) -export class UserPreferencesService { - - constructor( - private store: Store, - private attributeService: AttributeService - ) {} - - public loadUserPreferences(authUser: AuthUser): Observable { - return this.attributeService.getEntityAttributes({id: authUser.userId, entityType: EntityType.USER}, - AttributeScope.SERVER_SCOPE, [USER_PREFERENCES_ATTRIBUTE_KEY], { ignoreLoading: true, ignoreErrors: true }).pipe( - map((attributes) => { - const found = ((attributes || []).find(data => data.key === USER_PREFERENCES_ATTRIBUTE_KEY)); - return found ? JSON.parse(found.value) : initialUserPreferences; - }) - ); - } - - public saveUserPreferences(authUser: AuthUser, userPreferences: UserPreferences): Observable { - return this.attributeService.saveEntityAttributes({id: authUser.userId, entityType: EntityType.USER}, - AttributeScope.SERVER_SCOPE, - [{key: USER_PREFERENCES_ATTRIBUTE_KEY, value: JSON.stringify(userPreferences)}], - { ignoreLoading: true, ignoreErrors: true }); - } - -} diff --git a/ui-ngx/src/app/core/http/user-settings.service.ts b/ui-ngx/src/app/core/http/user-settings.service.ts new file mode 100644 index 0000000000..8aed9d1345 --- /dev/null +++ b/ui-ngx/src/app/core/http/user-settings.service.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig } from '@core/http/http-utils'; + +@Injectable({ + providedIn: 'root' +}) +export class UserSettingsService { + + constructor( + private http: HttpClient + ) {} + + public loadUserSettings(): Observable { + return this.http.get('/api/user/settings', defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})).pipe( + map((settings) => (!settings || !Object.keys(settings).length) ? initialUserSettings : settings) + ); + } + + public saveUserSettings(userSettings: UserSettings): Observable { + return this.http.post('/api/user/settings', userSettings, + defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})); + } + + public putUserSettings(userSettingsData: Partial): Observable { + return this.http.put('/api/user/settings', userSettingsData, + defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})); + } + + public deleteUserSettings(paths: string[]) { + return this.http.delete(`/api/user/settings/${paths.join(',')}`, + defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 39779b6f08..4831157536 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -47,6 +47,7 @@ import { DomSanitizer } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { TbInject } from '@shared/decorators/tb-inject'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { UserSettingsService } from '@core/http/user-settings.service'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -82,6 +83,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.dialogs = $injector.get(DialogService); this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.resourceService = $injector.get(ResourceService); + this.ctx.userSettingsService = $injector.get(UserSettingsService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.date = $injector.get(DatePipe); this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe); diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index e9e40c92a5..82ea632f7c 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -41,6 +41,7 @@ import { ResourceService } from '@core/http/resource.service'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { UserSettingsService } from '@core/http/user-settings.service'; export const ServicesMap = new Map>( [ @@ -69,6 +70,7 @@ export const ServicesMap = new Map>( ['authService', AuthService], ['resourceService', ResourceService], ['twoFactorAuthenticationService', TwoFactorAuthenticationService], - ['telemetryWsService', TelemetryWebsocketService] + ['telemetryWsService', TelemetryWebsocketService], + ['userSettingsService', UserSettingsService] ] ); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 6d8b710b85..9abc0a7c9d 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -89,6 +89,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; +import { UserSettingsService } from '@core/http/user-settings.service'; export interface IWidgetAction { name: string; @@ -179,6 +180,7 @@ export class WidgetContext { dialogs: DialogService; customDialog: CustomDialogService; resourceService: ResourceService; + userSettingsService: UserSettingsService; telemetryWsService: TelemetryWebsocketService; telemetrySubscribers?: TelemetrySubscriber[]; date: DatePipe; diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index 07d83158f8..36661ee3e4 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -49,7 +49,7 @@ export * from './rule-node.models'; export * from './settings.models'; export * from './tenant.model'; export * from './user.model'; -export * from './user-preferences.models'; +export * from './user-settings.models'; export * from './widget.models'; export * from './widgets-bundle.model'; export * from './window-message.model'; diff --git a/ui-ngx/src/app/shared/models/user-preferences.models.ts b/ui-ngx/src/app/shared/models/user-settings.models.ts similarity index 88% rename from ui-ngx/src/app/shared/models/user-preferences.models.ts rename to ui-ngx/src/app/shared/models/user-settings.models.ts index 7d4682967a..f64b12ea71 100644 --- a/ui-ngx/src/app/shared/models/user-preferences.models.ts +++ b/ui-ngx/src/app/shared/models/user-settings.models.ts @@ -14,10 +14,10 @@ /// limitations under the License. /// -export interface UserPreferences { +export interface UserSettings { openedMenuSections?: string[]; } -export const initialUserPreferences: UserPreferences = { +export const initialUserSettings: UserSettings = { openedMenuSections: [] };