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);
|
||||
}
|
||||
@ -96,19 +105,36 @@ export class MenuService {
|
||||
icon: 'mdi:alpha-t-box',
|
||||
isMdiIcon: true
|
||||
},
|
||||
{
|
||||
id: guid(),
|
||||
name: 'admin.resources',
|
||||
type: 'toggle',
|
||||
path: '/resources',
|
||||
height: '80px',
|
||||
icon: 'folder',
|
||||
pages: [
|
||||
{
|
||||
id: guid(),
|
||||
name: 'widget.widget-library',
|
||||
type: 'link',
|
||||
path: '/widgets-bundles',
|
||||
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,11 +161,11 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public validate(c: UntypedFormControl) {
|
||||
if (c.parent && !this.systemQueue) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/resources/widgets-bundles'
|
||||
},
|
||||
{
|
||||
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: 'widgets-bundles/:widgetsBundleId/widgetTypes',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes',
|
||||
},
|
||||
{
|
||||
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: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
|
||||
},
|
||||
{
|
||||
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: '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,7 +404,10 @@
|
||||
"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",
|
||||
|
||||
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