UI: Refactor menu for System administrator
This commit is contained in:
		
							parent
							
								
									17d1dd258a
								
							
						
					
					
						commit
						61e117f320
					
				@ -29,6 +29,11 @@
 | 
			
		||||
            "assets": [
 | 
			
		||||
              "src/thingsboard.ico",
 | 
			
		||||
              "src/assets",
 | 
			
		||||
              {
 | 
			
		||||
                "glob": "*.svg",
 | 
			
		||||
                "input": "./node_modules/@mdi/svg/svg/",
 | 
			
		||||
                "output": "/assets/mdi/"
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                "glob": "worker-html.js",
 | 
			
		||||
                "input": "./node_modules/ace-builds/src-noconflict/",
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@
 | 
			
		||||
    "@material-ui/core": "4.12.3",
 | 
			
		||||
    "@material-ui/icons": "4.11.2",
 | 
			
		||||
    "@material-ui/pickers": "3.3.10",
 | 
			
		||||
    "@mdi/svg": "^7.1.96",
 | 
			
		||||
    "@ngrx/effects": "^14.3.3",
 | 
			
		||||
    "@ngrx/store": "^14.3.3",
 | 
			
		||||
    "@ngrx/store-devtools": "^14.3.3",
 | 
			
		||||
 | 
			
		||||
@ -47,8 +47,13 @@ export class AppComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    console.log(`ThingsBoard Version: ${env.tbVersion}`);
 | 
			
		||||
 | 
			
		||||
    this.matIconRegistry.addSvgIconSetInNamespace('mdi',
 | 
			
		||||
      this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg'));
 | 
			
		||||
    this.matIconRegistry.addSvgIconResolver((name, namespace) => {
 | 
			
		||||
      if (namespace === 'mdi') {
 | 
			
		||||
        return this.domSanitizer.bypassSecurityTrustResourceUrl(`./assets/mdi/${name}.svg`);
 | 
			
		||||
      } else {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.matIconRegistry.addSvgIconLiteral(
 | 
			
		||||
      'google-logo',
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,8 @@ export enum AuthActionTypes {
 | 
			
		||||
  LOAD_USER = '[Auth] Load User',
 | 
			
		||||
  UPDATE_USER_DETAILS = '[Auth] Update User Details',
 | 
			
		||||
  UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
 | 
			
		||||
  UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository'
 | 
			
		||||
  UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository',
 | 
			
		||||
  UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ActionAuthAuthenticated implements Action {
 | 
			
		||||
@ -61,5 +62,12 @@ export class ActionAuthUpdateHasRepository implements Action {
 | 
			
		||||
  constructor(readonly payload: { hasRepository: boolean }) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ActionPreferencesUpdateOpenedMenuSection implements Action {
 | 
			
		||||
  readonly type = AuthActionTypes.UPDATE_OPENED_MENU_SECTION;
 | 
			
		||||
 | 
			
		||||
  constructor(readonly payload: { path: string; opened: boolean }) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
 | 
			
		||||
  ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository;
 | 
			
		||||
  ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
 | 
			
		||||
  ActionPreferencesUpdateOpenedMenuSection;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								ui-ngx/src/app/core/auth/auth.effects.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								ui-ngx/src/app/core/auth/auth.effects.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
///
 | 
			
		||||
/// 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 { 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 { mergeMap, withLatestFrom } from 'rxjs/operators';
 | 
			
		||||
import { AuthActions, AuthActionTypes } from '@core/auth/auth.actions';
 | 
			
		||||
import { selectAuthState } from '@core/auth/auth.selectors';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AuthEffects {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private actions$: Actions<AuthActions>,
 | 
			
		||||
    private store: Store<AppState>,
 | 
			
		||||
    private userPreferencesService: UserPreferencesService
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  persistUserPreferences = 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))
 | 
			
		||||
  ), {dispatch: false});
 | 
			
		||||
}
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { AuthUser, User } from '@shared/models/user.model';
 | 
			
		||||
import { UserPreferences } from '@shared/models/user-preferences.models';
 | 
			
		||||
 | 
			
		||||
export interface SysParamsState {
 | 
			
		||||
  userTokenAccessEnabled: boolean;
 | 
			
		||||
@ -22,6 +23,7 @@ export interface SysParamsState {
 | 
			
		||||
  edgesSupportEnabled: boolean;
 | 
			
		||||
  hasRepository: boolean;
 | 
			
		||||
  tbelEnabled: boolean;
 | 
			
		||||
  userPreferences: UserPreferences;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthPayload extends SysParamsState {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
 | 
			
		||||
import { AuthPayload, AuthState } from './auth.models';
 | 
			
		||||
import { AuthActions, AuthActionTypes } from './auth.actions';
 | 
			
		||||
import { initialUserPreferences } from '@shared/models/user-preferences.models';
 | 
			
		||||
 | 
			
		||||
const emptyUserAuthState: AuthPayload = {
 | 
			
		||||
  authUser: null,
 | 
			
		||||
@ -25,7 +26,8 @@ const emptyUserAuthState: AuthPayload = {
 | 
			
		||||
  allowedDashboardIds: [],
 | 
			
		||||
  edgesSupportEnabled: false,
 | 
			
		||||
  hasRepository: false,
 | 
			
		||||
  tbelEnabled: false
 | 
			
		||||
  tbelEnabled: false,
 | 
			
		||||
  userPreferences: initialUserPreferences
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const initialState: AuthState = {
 | 
			
		||||
@ -35,10 +37,10 @@ export const initialState: AuthState = {
 | 
			
		||||
  ...emptyUserAuthState
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function authReducer(
 | 
			
		||||
export const authReducer = (
 | 
			
		||||
  state: AuthState = initialState,
 | 
			
		||||
  action: AuthActions
 | 
			
		||||
): AuthState {
 | 
			
		||||
): AuthState => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case AuthActionTypes.AUTHENTICATED:
 | 
			
		||||
      return { ...state, isAuthenticated: true, ...action.payload };
 | 
			
		||||
@ -59,7 +61,19 @@ export function authReducer(
 | 
			
		||||
    case AuthActionTypes.UPDATE_HAS_REPOSITORY:
 | 
			
		||||
      return { ...state, ...action.payload};
 | 
			
		||||
 | 
			
		||||
    case AuthActionTypes.UPDATE_OPENED_MENU_SECTION:
 | 
			
		||||
      const openedMenuSections = new Set(state.userPreferences.openedMenuSections);
 | 
			
		||||
      if (action.payload.opened) {
 | 
			
		||||
        if (!openedMenuSections.has(action.payload.path)) {
 | 
			
		||||
          openedMenuSections.add(action.payload.path);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        openedMenuSections.delete(action.payload.path);
 | 
			
		||||
      }
 | 
			
		||||
      const userPreferences = {...state.userPreferences, ...{ openedMenuSections: Array.from(openedMenuSections)}};
 | 
			
		||||
      return { ...state, ...{ userPreferences }};
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -20,6 +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';
 | 
			
		||||
 | 
			
		||||
export const selectAuthState = createFeatureSelector< AuthState>(
 | 
			
		||||
  'auth'
 | 
			
		||||
@ -65,18 +66,45 @@ export const selectTbelEnabled = createSelector(
 | 
			
		||||
  (state: AuthState) => state.tbelEnabled
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export function getCurrentAuthState(store: Store<AppState>): AuthState {
 | 
			
		||||
export const selectUserPreferences = createSelector(
 | 
			
		||||
  selectAuthState,
 | 
			
		||||
  (state: AuthState) => state.userPreferences
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const selectOpenedMenuSections = createSelector(
 | 
			
		||||
  selectAuthState,
 | 
			
		||||
  (state: AuthState) => state.userPreferences.openedMenuSections
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const getCurrentAuthState = (store: Store<AppState>): AuthState => {
 | 
			
		||||
  let state: AuthState;
 | 
			
		||||
  store.pipe(select(selectAuth), take(1)).subscribe(
 | 
			
		||||
    val => state = val
 | 
			
		||||
  );
 | 
			
		||||
  return state;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getCurrentAuthUser(store: Store<AppState>): AuthUser {
 | 
			
		||||
export const getCurrentAuthUser = (store: Store<AppState>): AuthUser => {
 | 
			
		||||
  let authUser: AuthUser;
 | 
			
		||||
  store.pipe(select(selectAuthUser), take(1)).subscribe(
 | 
			
		||||
    val => authUser = val
 | 
			
		||||
  );
 | 
			
		||||
  return authUser;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getCurrentUserPreferences = (store: Store<AppState>): UserPreferences => {
 | 
			
		||||
  let userPreferences: UserPreferences;
 | 
			
		||||
  store.pipe(select(selectUserPreferences), take(1)).subscribe(
 | 
			
		||||
    val => userPreferences = val
 | 
			
		||||
  );
 | 
			
		||||
  return userPreferences;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getCurrentOpenedMenuSections = (store: Store<AppState>): string[] => {
 | 
			
		||||
  let openedMenuSections: string[];
 | 
			
		||||
  store.pipe(select(selectOpenedMenuSections), take(1)).subscribe(
 | 
			
		||||
    val => openedMenuSections = val
 | 
			
		||||
  );
 | 
			
		||||
  return openedMenuSections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -48,6 +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';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
    providedIn: 'root'
 | 
			
		||||
@ -66,6 +68,7 @@ export class AuthService {
 | 
			
		||||
    private dashboardService: DashboardService,
 | 
			
		||||
    private adminService: AdminService,
 | 
			
		||||
    private translate: TranslateService,
 | 
			
		||||
    private userPreferencesService: UserPreferencesService,
 | 
			
		||||
    private dialog: MatDialog
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
@ -490,12 +493,17 @@ export class AuthService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private loadUserPreferences(authUser: AuthUser): Observable<UserPreferences> {
 | 
			
		||||
    return this.userPreferencesService.loadUserPreferences(authUser);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> {
 | 
			
		||||
    const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
 | 
			
		||||
                     this.fetchAllowedDashboardIds(authPayload),
 | 
			
		||||
                     this.loadIsEdgesSupportEnabled(),
 | 
			
		||||
                     this.loadHasRepository(authPayload.authUser),
 | 
			
		||||
                     this.loadTbelEnabled(authPayload.authUser),
 | 
			
		||||
                     this.loadUserPreferences(authPayload.authUser),
 | 
			
		||||
                     this.timeService.loadMaxDatapointsLimit()];
 | 
			
		||||
    return forkJoin(sources)
 | 
			
		||||
      .pipe(map((data) => {
 | 
			
		||||
@ -504,10 +512,9 @@ export class AuthService {
 | 
			
		||||
        const edgesSupportEnabled: boolean = data[2] as boolean;
 | 
			
		||||
        const hasRepository: boolean = data[3] as boolean;
 | 
			
		||||
        const tbelEnabled: boolean = data[4] as boolean;
 | 
			
		||||
        return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled};
 | 
			
		||||
      }, catchError((err) => {
 | 
			
		||||
        return of({});
 | 
			
		||||
      })));
 | 
			
		||||
        const userPreferences = data[5] as UserPreferences;
 | 
			
		||||
        return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userPreferences};
 | 
			
		||||
      }, catchError((err) => of({}))));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable<LoginResponse> {
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
export * from './auth.actions';
 | 
			
		||||
export * from './auth.effects';
 | 
			
		||||
export * from './auth.models';
 | 
			
		||||
export * from './auth.reducer';
 | 
			
		||||
export * from './auth.selectors';
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ import { SettingsEffects } from '@app/core/settings/settings.effects';
 | 
			
		||||
import { NotificationState } from '@app/core/notification/notification.models';
 | 
			
		||||
import { notificationReducer } from '@app/core/notification/notification.reducer';
 | 
			
		||||
import { NotificationEffects } from '@app/core/notification/notification.effects';
 | 
			
		||||
import { AuthEffects } from '@core/auth/auth.effects';
 | 
			
		||||
 | 
			
		||||
export const reducers: ActionReducerMap<AppState> = {
 | 
			
		||||
  load: loadReducer,
 | 
			
		||||
@ -49,6 +50,7 @@ if (!env.production) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const effects: Type<any>[] = [
 | 
			
		||||
  AuthEffects,
 | 
			
		||||
  SettingsEffects,
 | 
			
		||||
  NotificationEffects
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -39,4 +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 './widget.service';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										58
									
								
								ui-ngx/src/app/core/http/user-preferences.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								ui-ngx/src/app/core/http/user-preferences.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
///
 | 
			
		||||
/// 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<AppState>,
 | 
			
		||||
    private attributeService: AttributeService
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  public loadUserPreferences(authUser: AuthUser): Observable<UserPreferences> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
    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 });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								ui-ngx/src/app/core/services/active-component.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								ui-ngx/src/app/core/services/active-component.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
///
 | 
			
		||||
/// 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, Subject } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class ActiveComponentService {
 | 
			
		||||
 | 
			
		||||
  private activeComponent: any;
 | 
			
		||||
  private activeComponentChangedSubject: Subject<any> = new Subject<any>();
 | 
			
		||||
 | 
			
		||||
  public getCurrentActiveComponent(): any {
 | 
			
		||||
    return this.activeComponent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setCurrentActiveComponent(component: any): void {
 | 
			
		||||
    this.activeComponent = component;
 | 
			
		||||
    this.activeComponentChangedSubject.next(component);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public onActiveComponentChanged(): Observable<any> {
 | 
			
		||||
    return this.activeComponentChangedSubject.asObservable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -26,6 +26,7 @@ export interface MenuSection extends HasUUID{
 | 
			
		||||
  isMdiIcon?: boolean;
 | 
			
		||||
  height?: string;
 | 
			
		||||
  pages?: Array<MenuSection>;
 | 
			
		||||
  opened?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface HomeSection {
 | 
			
		||||
 | 
			
		||||
@ -18,13 +18,14 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { AuthService } from '../auth/auth.service';
 | 
			
		||||
import { select, Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '../core.state';
 | 
			
		||||
import { selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
 | 
			
		||||
import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
 | 
			
		||||
import { take } from 'rxjs/operators';
 | 
			
		||||
import { HomeSection, MenuSection } from '@core/services/menu.models';
 | 
			
		||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
 | 
			
		||||
import { Authority } from '@shared/models/authority.enum';
 | 
			
		||||
import { guid } from '@core/utils';
 | 
			
		||||
import { AuthState } from '@core/auth/auth.models';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
@ -34,7 +35,9 @@ export class MenuService {
 | 
			
		||||
  menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
 | 
			
		||||
  homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
 | 
			
		||||
 | 
			
		||||
  constructor(private store: Store<AppState>, private authService: AuthService) {
 | 
			
		||||
  constructor(private store: Store<AppState>,
 | 
			
		||||
              private authService: AuthService,
 | 
			
		||||
              private router: Router) {
 | 
			
		||||
    this.store.pipe(select(selectIsAuthenticated)).subscribe(
 | 
			
		||||
      (authenticated: boolean) => {
 | 
			
		||||
        if (authenticated) {
 | 
			
		||||
@ -64,6 +67,12 @@ export class MenuService {
 | 
			
		||||
              homeSections = this.buildCustomerUserHome(authState);
 | 
			
		||||
              break;
 | 
			
		||||
          }
 | 
			
		||||
          const url = this.router.url;
 | 
			
		||||
          const openedMenuSections = getCurrentOpenedMenuSections(this.store);
 | 
			
		||||
          menuSections.filter(section => section.type === 'toggle' &&
 | 
			
		||||
                              (url.startsWith(section.path) || openedMenuSections.includes(section.path))).forEach(
 | 
			
		||||
            section => section.opened = true
 | 
			
		||||
          );
 | 
			
		||||
          this.menuSections$.next(menuSections);
 | 
			
		||||
          this.homeSections$.next(homeSections);
 | 
			
		||||
        }
 | 
			
		||||
@ -98,17 +107,34 @@ export class MenuService {
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: guid(),
 | 
			
		||||
        name: 'widget.widget-library',
 | 
			
		||||
        type: 'link',
 | 
			
		||||
        path: '/widgets-bundles',
 | 
			
		||||
        icon: 'now_widgets'
 | 
			
		||||
        name: 'admin.resources',
 | 
			
		||||
        type: 'toggle',
 | 
			
		||||
        path: '/resources',
 | 
			
		||||
        height: '80px',
 | 
			
		||||
        icon: 'folder',
 | 
			
		||||
        pages: [
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'widget.widget-library',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/resources/widgets-bundles',
 | 
			
		||||
            icon: 'now_widgets'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'resource.resources-library',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/resources/resources-library',
 | 
			
		||||
            icon: 'mdi:rhombus-split',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: guid(),
 | 
			
		||||
        name: 'admin.system-settings',
 | 
			
		||||
        type: 'toggle',
 | 
			
		||||
        type: 'link',
 | 
			
		||||
        path: '/settings',
 | 
			
		||||
        height: '320px',
 | 
			
		||||
        icon: 'settings',
 | 
			
		||||
        pages: [
 | 
			
		||||
          {
 | 
			
		||||
@ -127,40 +153,12 @@ export class MenuService {
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.sms-provider',
 | 
			
		||||
            name: 'admin.notifications',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/settings/sms-provider',
 | 
			
		||||
            icon: 'sms'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.security-settings',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/settings/security-settings',
 | 
			
		||||
            icon: 'security'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.oauth2.oauth2',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/settings/oauth2',
 | 
			
		||||
            icon: 'security'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.2fa.2fa',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/settings/2fa',
 | 
			
		||||
            icon: 'mdi:two-factor-authentication',
 | 
			
		||||
            path: '/settings/notifications',
 | 
			
		||||
            icon: 'mdi:message-badge',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'resource.resources-library',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/settings/resources-library',
 | 
			
		||||
            icon: 'folder'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.queues',
 | 
			
		||||
@ -169,6 +167,38 @@ export class MenuService {
 | 
			
		||||
            icon: 'swap_calls'
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: guid(),
 | 
			
		||||
        name: 'security.security',
 | 
			
		||||
        type: 'link',
 | 
			
		||||
        path: '/security-settings',
 | 
			
		||||
        icon: 'security',
 | 
			
		||||
        pages: [
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.general',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/security-settings/general',
 | 
			
		||||
            icon: 'settings_applications'
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.oauth2.oauth2',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/security-settings/oauth2',
 | 
			
		||||
            icon: 'mdi:shield-account',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: guid(),
 | 
			
		||||
            name: 'admin.2fa.2fa',
 | 
			
		||||
            type: 'link',
 | 
			
		||||
            path: '/security-settings/2fa',
 | 
			
		||||
            icon: 'mdi:two-factor-authentication',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
    return sections;
 | 
			
		||||
 | 
			
		||||
@ -21,9 +21,9 @@
 | 
			
		||||
      <div class="mat-toolbar-tools">
 | 
			
		||||
        <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
 | 
			
		||||
          <span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
 | 
			
		||||
          <mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection">
 | 
			
		||||
          <mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection && !attributeScopeSelectionReadonly">
 | 
			
		||||
            <mat-label translate>attribute.attributes-scope</mat-label>
 | 
			
		||||
            <mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly"
 | 
			
		||||
            <mat-select [disabled]="(isLoading$ | async)"
 | 
			
		||||
                        [ngModel]="attributeScope"
 | 
			
		||||
                        (ngModelChange)="attributeScopeChanged($event)">
 | 
			
		||||
              <mat-option *ngFor="let scope of attributeScopes" [value]="scope">
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,13 @@
 | 
			
		||||
-->
 | 
			
		||||
<mat-card class="settings-card">
 | 
			
		||||
  <mat-toolbar class="details-toolbar">
 | 
			
		||||
    <div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center">
 | 
			
		||||
    <div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
 | 
			
		||||
      <button mat-icon-button
 | 
			
		||||
              matTooltip="{{ 'action.back' | translate }}"
 | 
			
		||||
              matTooltipPosition="above"
 | 
			
		||||
              (click)="goBack()">
 | 
			
		||||
        <mat-icon>arrow_back</mat-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
      <div class="tb-details-title-header" fxLayout="column" fxLayoutAlign="center start">
 | 
			
		||||
        <div class="tb-details-title tb-ellipsis">{{ headerTitle }}</div>
 | 
			
		||||
        <div class="tb-details-subtitle tb-ellipsis">{{ headerSubtitle }}</div>
 | 
			
		||||
 | 
			
		||||
@ -144,6 +144,10 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
 | 
			
		||||
    return this.detailsForm;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  goBack(): void {
 | 
			
		||||
    this.router.navigate(['../'], { relativeTo: this.route });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private onUpdateEntity() {
 | 
			
		||||
    this.broadcast.broadcast('updateBreadcrumb');
 | 
			
		||||
    this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity);
 | 
			
		||||
@ -164,7 +168,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
 | 
			
		||||
      if (result) {
 | 
			
		||||
        this.entitiesTableConfig.deleteEntity(entity.id).subscribe(
 | 
			
		||||
          () => {
 | 
			
		||||
            this.router.navigate(['../'], {relativeTo: this.route});
 | 
			
		||||
            this.goBack();
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -175,10 +175,12 @@ import { AssetProfileDialogComponent } from '@home/components/profile/asset-prof
 | 
			
		||||
import { AssetProfileAutocompleteComponent } from '@home/components/profile/asset-profile-autocomplete.component';
 | 
			
		||||
import { MODULES_MAP } from '@shared/models/constants';
 | 
			
		||||
import { modulesMap } from '@modules/common/modules-map';
 | 
			
		||||
import { RouterTabsComponent } from '@home/components/router-tabs.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations:
 | 
			
		||||
    [
 | 
			
		||||
      RouterTabsComponent,
 | 
			
		||||
      EntitiesTableComponent,
 | 
			
		||||
      AddEntityDialogComponent,
 | 
			
		||||
      DetailsPanelComponent,
 | 
			
		||||
@ -332,6 +334,7 @@ import { modulesMap } from '@modules/common/modules-map';
 | 
			
		||||
    DeviceProfileCommonModule
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    RouterTabsComponent,
 | 
			
		||||
    EntitiesTableComponent,
 | 
			
		||||
    AddEntityDialogComponent,
 | 
			
		||||
    DetailsPanelComponent,
 | 
			
		||||
 | 
			
		||||
@ -161,9 +161,9 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr
 | 
			
		||||
      this.queueFormGroup.get('additionalInfo').get('description')
 | 
			
		||||
        .patchValue(this.modelValue.additionalInfo?.description, {emitEvent: false});
 | 
			
		||||
      this.submitStrategyTypeChanged();
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.disabled && !this.queueFormGroup.valid) {
 | 
			
		||||
      this.updateModel();
 | 
			
		||||
      if (!this.disabled && !this.queueFormGroup.valid) {
 | 
			
		||||
        this.updateModel();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,56 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
:host {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
 | 
			
		||||
  .mat-tab-nav-bar.tb-router-tabs {
 | 
			
		||||
    a.mat-tab-link {
 | 
			
		||||
      display: inline-flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      line-height: 40px;
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        border-bottom: none;
 | 
			
		||||
        background-color: rgba(255,255,255,0.08);
 | 
			
		||||
      }
 | 
			
		||||
      &:focus {
 | 
			
		||||
        border-bottom: none;
 | 
			
		||||
      }
 | 
			
		||||
      &.tb-active {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        background-color: rgba(255, 255, 255, .15);
 | 
			
		||||
      }
 | 
			
		||||
      mat-icon {
 | 
			
		||||
        margin-right: 8px;
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
      }
 | 
			
		||||
      span {
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        text-overflow: ellipsis;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tb-router-tabs-content {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,35 @@
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<div fxFlex fxLayout="column">
 | 
			
		||||
  <nav mat-tab-nav-bar backgroundColor="primary" class="tb-router-tabs">
 | 
			
		||||
    <a *ngFor="let tab of tabs$ | async"
 | 
			
		||||
       routerLink="{{tab.path}}"
 | 
			
		||||
       routerLinkActive
 | 
			
		||||
       #rla="routerLinkActive"
 | 
			
		||||
       mat-tab-link
 | 
			
		||||
       [ngClass]="{'tb-active': rla.isActive}"
 | 
			
		||||
       [active]="rla.isActive">
 | 
			
		||||
      <mat-icon *ngIf="!tab.isMdiIcon && tab.icon !== null" class="tb-mat-18">{{tab.icon}}</mat-icon>
 | 
			
		||||
      <mat-icon *ngIf="tab.isMdiIcon && tab.icon !== null" [svgIcon]="tab.icon" class="tb-mat-18"></mat-icon>
 | 
			
		||||
      <span>{{tab.name | translate}}</span>
 | 
			
		||||
    </a>
 | 
			
		||||
  </nav>
 | 
			
		||||
  <div fxFlex fxLayout="column" class="tb-router-tabs-content">
 | 
			
		||||
    <router-outlet (activate)="activeComponentChanged($event)"></router-outlet>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,65 @@
 | 
			
		||||
///
 | 
			
		||||
/// 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 { AfterViewInit, Component, Inject, OnInit } from '@angular/core';
 | 
			
		||||
import { PageComponent } from '@shared/components/page.component';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { WINDOW } from '@core/services/window.service';
 | 
			
		||||
import { BreakpointObserver } from '@angular/cdk/layout';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { MenuService } from '@core/services/menu.service';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { MenuSection } from '@core/services/menu.models';
 | 
			
		||||
import { instanceOfSearchableComponent } from '@home/models/searchable-component.models';
 | 
			
		||||
import { BroadcastService } from '@core/services/broadcast.service';
 | 
			
		||||
import { ActiveComponentService } from '@core/services/active-component.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-router-tabs',
 | 
			
		||||
  templateUrl: './router-tabs.component.html',
 | 
			
		||||
  styleUrls: ['./route-tabs.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class RouterTabsComponent extends PageComponent implements AfterViewInit, OnInit {
 | 
			
		||||
 | 
			
		||||
  tabs$: Observable<Array<MenuSection>> = this.menuService.menuSections().pipe(
 | 
			
		||||
    map(sections => {
 | 
			
		||||
      const found = sections.find(section => section.path === `/${this.activatedRoute.routeConfig.path}`);
 | 
			
		||||
      return found ? found.pages : [];
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  constructor(protected store: Store<AppState>,
 | 
			
		||||
              private activatedRoute: ActivatedRoute,
 | 
			
		||||
              public router: Router,
 | 
			
		||||
              private menuService: MenuService,
 | 
			
		||||
              private activeComponentService: ActiveComponentService,
 | 
			
		||||
              @Inject(WINDOW) private window: Window,
 | 
			
		||||
              public breakpointObserver: BreakpointObserver) {
 | 
			
		||||
    super(store);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {}
 | 
			
		||||
 | 
			
		||||
  activeComponentChanged(activeComponent: any) {
 | 
			
		||||
    this.activeComponentService.setCurrentActiveComponent(activeComponent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
  </mat-sidenav>
 | 
			
		||||
  <mat-sidenav-content>
 | 
			
		||||
    <div fxLayout="column" role="main" style="height: 100%;">
 | 
			
		||||
      <mat-toolbar fxLayout="row" color="primary" class="mat-elevation-z1 tb-primary-toolbar">
 | 
			
		||||
      <mat-toolbar fxLayout="row" color="primary" class="tb-primary-toolbar">
 | 
			
		||||
        <button [fxShow]="!forceFullscreen" mat-icon-button id="main"
 | 
			
		||||
                [ngClass]="{'tb-invisible': displaySearchMode()}"
 | 
			
		||||
                fxHide.gt-sm (click)="sidenav.toggle()">
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
          <mat-icon class="material-icons">arrow_back</mat-icon>
 | 
			
		||||
        </button>
 | 
			
		||||
        <tb-breadcrumb [fxShow]="!displaySearchMode()"
 | 
			
		||||
             fxFlex [activeComponent]="activeComponent" class="mat-toolbar-tools">
 | 
			
		||||
             fxFlex class="mat-toolbar-tools">
 | 
			
		||||
        </tb-breadcrumb>
 | 
			
		||||
        <div [fxShow]="displaySearchMode()" fxFlex fxLayout="row" class="tb-dark">
 | 
			
		||||
          <mat-form-field fxFlex floatLabel="always">
 | 
			
		||||
@ -73,7 +73,7 @@
 | 
			
		||||
        <tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu>
 | 
			
		||||
      </mat-toolbar>
 | 
			
		||||
      <mat-progress-bar color="warn" style="z-index: 10; margin-bottom: -4px; width: 100%;" mode="indeterminate"
 | 
			
		||||
                        *ngIf="isLoading$ | async">
 | 
			
		||||
                        *ngIf="!hideLoadingBar && (isLoading$ | async)">
 | 
			
		||||
      </mat-progress-bar>
 | 
			
		||||
      <div fxFlex fxLayout="column" tb-toast class="tb-main-content">
 | 
			
		||||
        <router-outlet (activate)="activeComponentChanged($event)"></router-outlet>
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,8 @@ import { MatSidenav } from '@angular/material/sidenav';
 | 
			
		||||
import { AuthState } from '@core/auth/auth.models';
 | 
			
		||||
import { WINDOW } from '@core/services/window.service';
 | 
			
		||||
import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models';
 | 
			
		||||
import { ActiveComponentService } from '@core/services/active-component.service';
 | 
			
		||||
import { RouterTabsComponent } from '@home/components/router-tabs.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-home',
 | 
			
		||||
@ -65,8 +67,11 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
 | 
			
		||||
  showSearch = false;
 | 
			
		||||
  searchText = '';
 | 
			
		||||
 | 
			
		||||
  hideLoadingBar = false;
 | 
			
		||||
 | 
			
		||||
  constructor(protected store: Store<AppState>,
 | 
			
		||||
              @Inject(WINDOW) private window: Window,
 | 
			
		||||
              private activeComponentService: ActiveComponentService,
 | 
			
		||||
              public breakpointObserver: BreakpointObserver) {
 | 
			
		||||
    super(store);
 | 
			
		||||
  }
 | 
			
		||||
@ -133,6 +138,8 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
 | 
			
		||||
    this.showSearch = false;
 | 
			
		||||
    this.searchText = '';
 | 
			
		||||
    this.activeComponent = activeComponent;
 | 
			
		||||
    this.hideLoadingBar = activeComponent && activeComponent instanceof RouterTabsComponent;
 | 
			
		||||
    this.activeComponentService.setCurrentActiveComponent(activeComponent);
 | 
			
		||||
    if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) {
 | 
			
		||||
      this.searchEnabled = true;
 | 
			
		||||
      this.searchableComponent = this.activeComponent;
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,8 @@
 | 
			
		||||
    limitations under the License.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
 | 
			
		||||
  <mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon>
 | 
			
		||||
  <mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon>
 | 
			
		||||
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'subset', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
 | 
			
		||||
  <mat-icon *ngIf="!section.isMdiIcon && section.icon !== null" class="tb-mat-18">{{section.icon}}</mat-icon>
 | 
			
		||||
  <mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon" class="tb-mat-18"></mat-icon>
 | 
			
		||||
  <span>{{section.name | translate}}</span>
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
@ -15,13 +15,12 @@
 | 
			
		||||
    limitations under the License.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<a mat-button class="tb-button-toggle"
 | 
			
		||||
        routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
 | 
			
		||||
  <mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon>
 | 
			
		||||
  <mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon>
 | 
			
		||||
<a mat-button class="tb-button-toggle" (click)="toggleSection()">
 | 
			
		||||
  <mat-icon *ngIf="!section.isMdiIcon && section.icon !== null" class="tb-mat-18">{{section.icon}}</mat-icon>
 | 
			
		||||
  <mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon" class="tb-mat-18"></mat-icon>
 | 
			
		||||
  <span>{{section.name | translate}}</span>
 | 
			
		||||
  <span class=" pull-right fa fa-chevron-down tb-toggle-icon"
 | 
			
		||||
        [ngClass]="{'tb-toggled' : sectionActive()}"></span>
 | 
			
		||||
        [ngClass]="{'tb-toggled' : section.opened}"></span>
 | 
			
		||||
</a>
 | 
			
		||||
<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}">
 | 
			
		||||
  <li *ngFor="let page of section.pages; trackBy: trackBySectionPages">
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,9 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { MenuSection } from '@core/services/menu.models';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { ActionPreferencesUpdateOpenedMenuSection } from '@core/auth/auth.actions';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-menu-toggle',
 | 
			
		||||
@ -28,24 +31,26 @@ export class MenuToggleComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @Input() section: MenuSection;
 | 
			
		||||
 | 
			
		||||
  constructor(private router: Router) {
 | 
			
		||||
  constructor(private router: Router,
 | 
			
		||||
              private store: Store<AppState>) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sectionActive(): boolean {
 | 
			
		||||
    return this.router.isActive(this.section.path, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sectionHeight(): string {
 | 
			
		||||
    if (this.router.isActive(this.section.path, false)) {
 | 
			
		||||
    if (this.section.opened) {
 | 
			
		||||
      return this.section.height;
 | 
			
		||||
    } else {
 | 
			
		||||
      return '0px';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleSection() {
 | 
			
		||||
    this.section.opened = !this.section.opened;
 | 
			
		||||
    this.store.dispatch(new ActionPreferencesUpdateOpenedMenuSection({path: this.section.path, opened: this.section.opened}));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackBySectionPages(index: number, section: MenuSection){
 | 
			
		||||
    return section.id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,16 @@ import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-
 | 
			
		||||
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
 | 
			
		||||
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
 | 
			
		||||
import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component';
 | 
			
		||||
import { WidgetsBundlesTableConfigResolver } from '@home/pages/widget/widgets-bundles-table-config.resolver';
 | 
			
		||||
import {
 | 
			
		||||
  WidgetEditorAddDataResolver, widgetEditorBreadcumbLabelFunction,
 | 
			
		||||
  WidgetEditorDataResolver,
 | 
			
		||||
  WidgetsBundleResolver,
 | 
			
		||||
  WidgetsTypesDataResolver, widgetTypesBreadcumbLabelFunction
 | 
			
		||||
} from '@home/pages/widget/widget-library-routing.module';
 | 
			
		||||
import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
 | 
			
		||||
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
 | 
			
		||||
import { RouterTabsComponent } from '@home/components/router-tabs.component';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
 | 
			
		||||
@ -49,8 +59,146 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: 'resources',
 | 
			
		||||
    data: {
 | 
			
		||||
      auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
        label: 'admin.resources',
 | 
			
		||||
        icon: 'folder'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        children: [],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
          redirectTo: '/resources/widgets-bundles'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'widgets-bundles',
 | 
			
		||||
        data: {
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'widgets-bundle.widgets-bundles',
 | 
			
		||||
            icon: 'now_widgets'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: EntitiesTableComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
              title: 'widgets-bundle.widgets-bundles'
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              entitiesTableConfig: WidgetsBundlesTableConfigResolver
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: ':widgetsBundleId/widgetTypes',
 | 
			
		||||
            data: {
 | 
			
		||||
              breadcrumb: {
 | 
			
		||||
                labelFunction: widgetTypesBreadcumbLabelFunction,
 | 
			
		||||
                icon: 'now_widgets'
 | 
			
		||||
              } as BreadCrumbConfig<any>
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              widgetsBundle: WidgetsBundleResolver
 | 
			
		||||
            },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: '',
 | 
			
		||||
                component: WidgetLibraryComponent,
 | 
			
		||||
                data: {
 | 
			
		||||
                  auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
                  title: 'widget.widget-library'
 | 
			
		||||
                },
 | 
			
		||||
                resolve: {
 | 
			
		||||
                  widgetsData: WidgetsTypesDataResolver
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: ':widgetTypeId',
 | 
			
		||||
                component: WidgetEditorComponent,
 | 
			
		||||
                canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
                data: {
 | 
			
		||||
                  auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
                  title: 'widget.editor',
 | 
			
		||||
                  breadcrumb: {
 | 
			
		||||
                    labelFunction: widgetEditorBreadcumbLabelFunction,
 | 
			
		||||
                    icon: 'insert_chart'
 | 
			
		||||
                  } as BreadCrumbConfig<WidgetEditorComponent>
 | 
			
		||||
                },
 | 
			
		||||
                resolve: {
 | 
			
		||||
                  widgetEditorData: WidgetEditorDataResolver
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'add/:widgetType',
 | 
			
		||||
                component: WidgetEditorComponent,
 | 
			
		||||
                canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
                data: {
 | 
			
		||||
                  auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
                  title: 'widget.editor',
 | 
			
		||||
                  breadcrumb: {
 | 
			
		||||
                    labelFunction: widgetEditorBreadcumbLabelFunction,
 | 
			
		||||
                    icon: 'insert_chart'
 | 
			
		||||
                  } as BreadCrumbConfig<WidgetEditorComponent>
 | 
			
		||||
                },
 | 
			
		||||
                resolve: {
 | 
			
		||||
                  widgetEditorData: WidgetEditorAddDataResolver
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'resources-library',
 | 
			
		||||
        data: {
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'resource.resources-library',
 | 
			
		||||
            icon: 'mdi:rhombus-split'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: EntitiesTableComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
 | 
			
		||||
              title: 'resource.resources-library',
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              entitiesTableConfig: ResourcesLibraryTableConfigResolver
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: ':entityId',
 | 
			
		||||
            component: EntityDetailsPageComponent,
 | 
			
		||||
            canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
            data: {
 | 
			
		||||
              breadcrumb: {
 | 
			
		||||
                labelFunction: entityDetailsPageBreadcrumbLabelFunction,
 | 
			
		||||
                icon: 'mdi:rhombus-split'
 | 
			
		||||
              } as BreadCrumbConfig<EntityDetailsPageComponent>,
 | 
			
		||||
              auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
 | 
			
		||||
              title: 'resource.resources-library'
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              entitiesTableConfig: ResourcesLibraryTableConfigResolver
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'settings',
 | 
			
		||||
    component: RouterTabsComponent,
 | 
			
		||||
    data: {
 | 
			
		||||
      auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
@ -97,98 +245,18 @@ const routes: Routes = [
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'sms-provider',
 | 
			
		||||
        path: 'notifications',
 | 
			
		||||
        component: SmsProviderComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.sms-provider-settings',
 | 
			
		||||
          title: 'admin.notifications-settings',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.sms-provider',
 | 
			
		||||
            icon: 'sms'
 | 
			
		||||
            label: 'admin.notifications',
 | 
			
		||||
            icon: 'mdi:message-badge'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'security-settings',
 | 
			
		||||
        component: SecuritySettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.security-settings',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.security-settings',
 | 
			
		||||
            icon: 'security'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'oauth2',
 | 
			
		||||
        component: OAuth2SettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.oauth2.oauth2',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.oauth2.oauth2',
 | 
			
		||||
            icon: 'security'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        resolve: {
 | 
			
		||||
          loginProcessingUrl: OAuth2LoginProcessingUrlResolver
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'home',
 | 
			
		||||
        component: HomeSettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.TENANT_ADMIN],
 | 
			
		||||
          title: 'admin.home-settings',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.home-settings',
 | 
			
		||||
            icon: 'settings_applications'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'resources-library',
 | 
			
		||||
        data: {
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'resource.resources-library',
 | 
			
		||||
            icon: 'folder'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: EntitiesTableComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
 | 
			
		||||
              title: 'resource.resources-library',
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              entitiesTableConfig: ResourcesLibraryTableConfigResolver
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: ':entityId',
 | 
			
		||||
            component: EntityDetailsPageComponent,
 | 
			
		||||
            canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
            data: {
 | 
			
		||||
              breadcrumb: {
 | 
			
		||||
                labelFunction: entityDetailsPageBreadcrumbLabelFunction,
 | 
			
		||||
                icon: 'folder'
 | 
			
		||||
              } as BreadCrumbConfig<EntityDetailsPageComponent>,
 | 
			
		||||
              auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
 | 
			
		||||
              title: 'resource.resources-library'
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              entitiesTableConfig: ResourcesLibraryTableConfigResolver
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'queues',
 | 
			
		||||
        data: {
 | 
			
		||||
@ -228,16 +296,15 @@ const routes: Routes = [
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '2fa',
 | 
			
		||||
        component: TwoFactorAuthSettingsComponent,
 | 
			
		||||
        path: 'home',
 | 
			
		||||
        component: HomeSettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.2fa.2fa',
 | 
			
		||||
          auth: [Authority.TENANT_ADMIN],
 | 
			
		||||
          title: 'admin.home-settings',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.2fa.2fa',
 | 
			
		||||
            icon: 'mdi:two-factor-authentication',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
            label: 'admin.home-settings',
 | 
			
		||||
            icon: 'settings_applications'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
@ -266,6 +333,98 @@ const routes: Routes = [
 | 
			
		||||
            icon: 'settings_backup_restore'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'security-settings',
 | 
			
		||||
        redirectTo: '/security-settings/general'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'oauth2',
 | 
			
		||||
        redirectTo: '/security-settings/oauth2'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'resources-library',
 | 
			
		||||
        pathMatch: 'full',
 | 
			
		||||
        redirectTo: '/resources/resources-library'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'resources-library/:entityId',
 | 
			
		||||
        redirectTo: '/resources/resources-library/:entityId'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '2fa',
 | 
			
		||||
        redirectTo: '/security-settings/2fa'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'sms-provider',
 | 
			
		||||
        redirectTo: '/settings/notifications'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'security-settings',
 | 
			
		||||
    component: RouterTabsComponent,
 | 
			
		||||
    data: {
 | 
			
		||||
      auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
        label: 'security.security',
 | 
			
		||||
        icon: 'security'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        children: [],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
          redirectTo: {
 | 
			
		||||
            SYS_ADMIN: '/security-settings/general',
 | 
			
		||||
            TENANT_ADMIN: '/security-settings/audit-logs'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'general',
 | 
			
		||||
        component: SecuritySettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.general',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.general',
 | 
			
		||||
            icon: 'settings_applications'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '2fa',
 | 
			
		||||
        component: TwoFactorAuthSettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.2fa.2fa',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.2fa.2fa',
 | 
			
		||||
            icon: 'mdi:two-factor-authentication',
 | 
			
		||||
            isMdiIcon: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'oauth2',
 | 
			
		||||
        component: OAuth2SettingsComponent,
 | 
			
		||||
        canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN],
 | 
			
		||||
          title: 'admin.oauth2.oauth2',
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            label: 'admin.oauth2.oauth2',
 | 
			
		||||
            icon: 'mdi:shield-account'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        resolve: {
 | 
			
		||||
          loginProcessingUrl: OAuth2LoginProcessingUrlResolver
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
@ -277,7 +436,12 @@ const routes: Routes = [
 | 
			
		||||
  providers: [
 | 
			
		||||
    OAuth2LoginProcessingUrlResolver,
 | 
			
		||||
    ResourcesLibraryTableConfigResolver,
 | 
			
		||||
    QueuesTableConfigResolver
 | 
			
		||||
    QueuesTableConfigResolver,
 | 
			
		||||
    WidgetsBundlesTableConfigResolver,
 | 
			
		||||
    WidgetsBundleResolver,
 | 
			
		||||
    WidgetsTypesDataResolver,
 | 
			
		||||
    WidgetEditorDataResolver,
 | 
			
		||||
    WidgetEditorAddDataResolver
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class AdminRoutingModule { }
 | 
			
		||||
 | 
			
		||||
@ -123,7 +123,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
 | 
			
		||||
    if ($event) {
 | 
			
		||||
      $event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
    const url = this.router.createUrlTree(['settings', 'resources-library', resourceInfo.id.id]);
 | 
			
		||||
    const url = this.router.createUrlTree(['resources', 'resources-library', resourceInfo.id.id]);
 | 
			
		||||
    this.router.navigateByUrl(url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
  <mat-card class="settings-card">
 | 
			
		||||
    <mat-card-title>
 | 
			
		||||
      <div fxLayout="row">
 | 
			
		||||
        <span class="mat-headline" translate>admin.sms-provider-settings</span>
 | 
			
		||||
        <span class="mat-headline" translate>admin.notifications-settings</span>
 | 
			
		||||
        <span fxFlex></span>
 | 
			
		||||
        <div tb-help="smsProviderSettings"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,6 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
 | 
			
		||||
 | 
			
		||||
export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction<EntityDetailsPageComponent>
 | 
			
		||||
  = ((route, translate, component) => {
 | 
			
		||||
  return component.entity?.name || component.headerSubtitle;
 | 
			
		||||
  return component.entity?.name;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,8 +36,3 @@
 | 
			
		||||
         label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
 | 
			
		||||
  <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
 | 
			
		||||
</mat-tab>
 | 
			
		||||
<mat-tab *ngIf="entity"
 | 
			
		||||
         label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
 | 
			
		||||
  <tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
 | 
			
		||||
                  [entityId]="entity.id"></tb-event-table>
 | 
			
		||||
</mat-tab>
 | 
			
		||||
 | 
			
		||||
@ -123,82 +123,22 @@ export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetE
 | 
			
		||||
export const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: 'widgets-bundles',
 | 
			
		||||
    data: {
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
        label: 'widgets-bundle.widgets-bundles',
 | 
			
		||||
        icon: 'now_widgets'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: EntitiesTableComponent,
 | 
			
		||||
        data: {
 | 
			
		||||
          auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
          title: 'widgets-bundle.widgets-bundles'
 | 
			
		||||
        },
 | 
			
		||||
        resolve: {
 | 
			
		||||
          entitiesTableConfig: WidgetsBundlesTableConfigResolver
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: ':widgetsBundleId/widgetTypes',
 | 
			
		||||
        data: {
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
            labelFunction: widgetTypesBreadcumbLabelFunction,
 | 
			
		||||
            icon: 'now_widgets'
 | 
			
		||||
          } as BreadCrumbConfig<any>
 | 
			
		||||
        },
 | 
			
		||||
        resolve: {
 | 
			
		||||
          widgetsBundle: WidgetsBundleResolver
 | 
			
		||||
        },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: WidgetLibraryComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
              title: 'widget.widget-library'
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              widgetsData: WidgetsTypesDataResolver
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: ':widgetTypeId',
 | 
			
		||||
            component: WidgetEditorComponent,
 | 
			
		||||
            canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
              title: 'widget.editor',
 | 
			
		||||
              breadcrumb: {
 | 
			
		||||
                labelFunction: widgetEditorBreadcumbLabelFunction,
 | 
			
		||||
                icon: 'insert_chart'
 | 
			
		||||
              } as BreadCrumbConfig<WidgetEditorComponent>
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              widgetEditorData: WidgetEditorDataResolver
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'add/:widgetType',
 | 
			
		||||
            component: WidgetEditorComponent,
 | 
			
		||||
            canDeactivate: [ConfirmOnExitGuard],
 | 
			
		||||
            data: {
 | 
			
		||||
              auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
 | 
			
		||||
              title: 'widget.editor',
 | 
			
		||||
              breadcrumb: {
 | 
			
		||||
                labelFunction: widgetEditorBreadcumbLabelFunction,
 | 
			
		||||
                icon: 'insert_chart'
 | 
			
		||||
              } as BreadCrumbConfig<WidgetEditorComponent>
 | 
			
		||||
            },
 | 
			
		||||
            resolve: {
 | 
			
		||||
              widgetEditorData: WidgetEditorAddDataResolver
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
    pathMatch: 'full',
 | 
			
		||||
    redirectTo: '/resources/widgets-bundles'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'widgets-bundles/:widgetsBundleId/widgetTypes',
 | 
			
		||||
    pathMatch: 'full',
 | 
			
		||||
    redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
 | 
			
		||||
    pathMatch: 'full',
 | 
			
		||||
    redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
 | 
			
		||||
    redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
 | 
			
		||||
  }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -206,12 +146,6 @@ export const routes: Routes = [
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [RouterModule.forChild(routes)],
 | 
			
		||||
  exports: [RouterModule],
 | 
			
		||||
  providers: [
 | 
			
		||||
    WidgetsBundlesTableConfigResolver,
 | 
			
		||||
    WidgetsBundleResolver,
 | 
			
		||||
    WidgetsTypesDataResolver,
 | 
			
		||||
    WidgetEditorDataResolver,
 | 
			
		||||
    WidgetEditorAddDataResolver
 | 
			
		||||
  ]
 | 
			
		||||
  providers: []
 | 
			
		||||
})
 | 
			
		||||
export class WidgetLibraryRoutingModule { }
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
 | 
			
		||||
    if ($event) {
 | 
			
		||||
      $event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
    this.router.navigateByUrl(`widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
 | 
			
		||||
    this.router.navigateByUrl(`resources/widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
 | 
			
		||||
 | 
			
		||||
@ -17,25 +17,32 @@
 | 
			
		||||
-->
 | 
			
		||||
<div fxFlex class="tb-breadcrumb" fxLayout="row">
 | 
			
		||||
  <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
 | 
			
		||||
    {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
 | 
			
		||||
    {{ breadcrumb.ignoreTranslate
 | 
			
		||||
    ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label))
 | 
			
		||||
    : (breadcrumb.label | translate) }}
 | 
			
		||||
  </h1>
 | 
			
		||||
  <span fxHide.lt-md fxLayout="row" *ngFor="let breadcrumb of breadcrumbs$ | async; trackBy: trackByBreadcrumbs; last as isLast;" [ngSwitch]="isLast">
 | 
			
		||||
	    <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
 | 
			
		||||
        <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
 | 
			
		||||
	    	</mat-icon>
 | 
			
		||||
        <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
 | 
			
		||||
          {{ breadcrumb.icon }}
 | 
			
		||||
        </mat-icon>
 | 
			
		||||
        {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
 | 
			
		||||
        <ng-container
 | 
			
		||||
          *ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
 | 
			
		||||
        </ng-container>
 | 
			
		||||
	    </a>
 | 
			
		||||
	    <span *ngSwitchCase="true">
 | 
			
		||||
        <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
 | 
			
		||||
	    	</mat-icon>
 | 
			
		||||
	    	<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
 | 
			
		||||
	    	  {{ breadcrumb.icon }}
 | 
			
		||||
	    	</mat-icon>
 | 
			
		||||
        {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
 | 
			
		||||
        <ng-container
 | 
			
		||||
          *ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </span>
 | 
			
		||||
	    <span class="divider" [fxHide]="isLast"> > </span>
 | 
			
		||||
	</span>
 | 
			
		||||
</div>
 | 
			
		||||
<ng-template #breadcrumbWithIcon let-breadcrumb="breadcrumb">
 | 
			
		||||
  <img *ngIf="breadcrumb.iconUrl" [src]="breadcrumb.iconUrl"/>
 | 
			
		||||
  <mat-icon *ngIf="!breadcrumb.iconUrl && breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
 | 
			
		||||
  </mat-icon>
 | 
			
		||||
  <mat-icon *ngIf="!breadcrumb.iconUrl && !breadcrumb.isMdiIcon">
 | 
			
		||||
    {{ breadcrumb.icon }}
 | 
			
		||||
  </mat-icon>
 | 
			
		||||
  {{ breadcrumb.ignoreTranslate
 | 
			
		||||
  ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label))
 | 
			
		||||
  : (breadcrumb.label | translate) }}
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -18,10 +18,12 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy
 | 
			
		||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
 | 
			
		||||
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
 | 
			
		||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
 | 
			
		||||
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
 | 
			
		||||
import { TranslateService } from '@ngx-translate/core';
 | 
			
		||||
import { guid } from '@core/utils';
 | 
			
		||||
import { BroadcastService } from '@core/services/broadcast.service';
 | 
			
		||||
import { ActiveComponentService } from '@core/services/active-component.service';
 | 
			
		||||
import { UtilsService } from '@core/services/utils.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-breadcrumb',
 | 
			
		||||
@ -34,8 +36,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
 | 
			
		||||
  activeComponentValue: any;
 | 
			
		||||
  updateBreadcrumbsSubscription: Subscription = null;
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  set activeComponent(activeComponent: any) {
 | 
			
		||||
  setActiveComponent(activeComponent: any) {
 | 
			
		||||
    if (this.updateBreadcrumbsSubscription) {
 | 
			
		||||
      this.updateBreadcrumbsSubscription.unsubscribe();
 | 
			
		||||
      this.updateBreadcrumbsSubscription = null;
 | 
			
		||||
@ -48,7 +49,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot));
 | 
			
		||||
  breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>([]);
 | 
			
		||||
 | 
			
		||||
  routerEventsSubscription = this.router.events.pipe(
 | 
			
		||||
    filter((event) => event instanceof NavigationEnd ),
 | 
			
		||||
@ -56,6 +57,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
 | 
			
		||||
    map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) )
 | 
			
		||||
  ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) );
 | 
			
		||||
 | 
			
		||||
  activeComponentSubscription = this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp));
 | 
			
		||||
 | 
			
		||||
  lastBreadcrumb$ = this.breadcrumbs$.pipe(
 | 
			
		||||
    map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1])
 | 
			
		||||
  );
 | 
			
		||||
@ -63,20 +66,26 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
 | 
			
		||||
  constructor(private router: Router,
 | 
			
		||||
              private activatedRoute: ActivatedRoute,
 | 
			
		||||
              private broadcast: BroadcastService,
 | 
			
		||||
              private activeComponentService: ActiveComponentService,
 | 
			
		||||
              private cd: ChangeDetectorRef,
 | 
			
		||||
              private translate: TranslateService) {
 | 
			
		||||
              private translate: TranslateService,
 | 
			
		||||
              public utils: UtilsService) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.broadcast.on('updateBreadcrumb', () => {
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    });
 | 
			
		||||
    this.setActiveComponent(this.activeComponentService.getCurrentActiveComponent());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    if (this.routerEventsSubscription) {
 | 
			
		||||
      this.routerEventsSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.activeComponentSubscription) {
 | 
			
		||||
      this.activeComponentSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private lastChild(route: ActivatedRouteSnapshot) {
 | 
			
		||||
@ -100,9 +109,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
 | 
			
		||||
        let labelFunction;
 | 
			
		||||
        let ignoreTranslate;
 | 
			
		||||
        if (breadcrumbConfig.labelFunction) {
 | 
			
		||||
          labelFunction = () => {
 | 
			
		||||
            return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data);
 | 
			
		||||
          };
 | 
			
		||||
          labelFunction = () => breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data);
 | 
			
		||||
          ignoreTranslate = true;
 | 
			
		||||
        } else {
 | 
			
		||||
          label = breadcrumbConfig.label || 'home.home';
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,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 './widget.models';
 | 
			
		||||
export * from './widgets-bundle.model';
 | 
			
		||||
export * from './window-message.model';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								ui-ngx/src/app/shared/models/user-preferences.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ui-ngx/src/app/shared/models/user-preferences.models.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
///
 | 
			
		||||
/// 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.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
export interface UserPreferences {
 | 
			
		||||
  openedMenuSections?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const initialUserPreferences: UserPreferences = {
 | 
			
		||||
  openedMenuSections: []
 | 
			
		||||
};
 | 
			
		||||
@ -404,8 +404,11 @@
 | 
			
		||||
            "generate-key": "Generate key",
 | 
			
		||||
            "info-header": "All users will be to re-logined",
 | 
			
		||||
            "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets."
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
        },
 | 
			
		||||
        "resources": "Resources",
 | 
			
		||||
        "notifications": "Notifications",
 | 
			
		||||
        "notifications-settings": "Notifications settings"
 | 
			
		||||
    },
 | 
			
		||||
    "alarm": {
 | 
			
		||||
        "alarm": "Alarm",
 | 
			
		||||
        "alarms": "Alarms",
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 Before Width: | Height: | Size: 2.3 MiB  | 
@ -1701,6 +1701,11 @@
 | 
			
		||||
    prop-types "^15.7.2"
 | 
			
		||||
    react-is "^16.8.0 || ^17.0.0"
 | 
			
		||||
 | 
			
		||||
"@mdi/svg@^7.1.96":
 | 
			
		||||
  version "7.1.96"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-7.1.96.tgz#4911ee6d2f3457f016be8b7a454cf1a8e5c361f0"
 | 
			
		||||
  integrity sha512-QO+CyF7eZsYBJpyb9Q77r1O6PFdp/Ircx8FMV7+cFS7g0p5rF55PA9zrmzuZqi1LyPKANDpr0oULNLHgeQuXZQ==
 | 
			
		||||
 | 
			
		||||
"@ngrx/effects@^14.3.3":
 | 
			
		||||
  version "14.3.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-14.3.3.tgz#5c9e43e4dc5e1d544f4604fc6a32feec2380c413"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user