Browse Source

UI: Refactor menu for System administrator

pull/8247/head
Igor Kulikov 4 years ago
parent
commit
61e117f320
  1. 5
      ui-ngx/angular.json
  2. 1
      ui-ngx/package.json
  3. 9
      ui-ngx/src/app/app.component.ts
  4. 12
      ui-ngx/src/app/core/auth/auth.actions.ts
  5. 42
      ui-ngx/src/app/core/auth/auth.effects.ts
  6. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  7. 22
      ui-ngx/src/app/core/auth/auth.reducer.ts
  8. 36
      ui-ngx/src/app/core/auth/auth.selectors.ts
  9. 15
      ui-ngx/src/app/core/auth/auth.service.ts
  10. 1
      ui-ngx/src/app/core/auth/public-api.ts
  11. 2
      ui-ngx/src/app/core/core.state.ts
  12. 1
      ui-ngx/src/app/core/http/public-api.ts
  13. 58
      ui-ngx/src/app/core/http/user-preferences.service.ts
  14. 41
      ui-ngx/src/app/core/services/active-component.service.ts
  15. 1
      ui-ngx/src/app/core/services/menu.models.ts
  16. 92
      ui-ngx/src/app/core/services/menu.service.ts
  17. 4
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  18. 8
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  19. 6
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  20. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  21. 6
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts
  22. 56
      ui-ngx/src/app/modules/home/components/route-tabs.component.scss
  23. 35
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  24. 65
      ui-ngx/src/app/modules/home/components/router-tabs.component.ts
  25. 6
      ui-ngx/src/app/modules/home/home.component.html
  26. 7
      ui-ngx/src/app/modules/home/home.component.ts
  27. 6
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  28. 9
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  29. 17
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts
  30. 346
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  31. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  32. 2
      ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html
  33. 2
      ui-ngx/src/app/modules/home/pages/home-pages.models.ts
  34. 5
      ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html
  35. 100
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  36. 2
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  37. 33
      ui-ngx/src/app/shared/components/breadcrumb.component.html
  38. 23
      ui-ngx/src/app/shared/components/breadcrumb.component.ts
  39. 1
      ui-ngx/src/app/shared/models/public-api.ts
  40. 23
      ui-ngx/src/app/shared/models/user-preferences.models.ts
  41. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 1
      ui-ngx/src/assets/mdi.svg
  43. 5
      ui-ngx/yarn.lock

5
ui-ngx/angular.json

@ -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/",

1
ui-ngx/package.json

@ -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",

9
ui-ngx/src/app/app.component.ts

@ -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',

12
ui-ngx/src/app/core/auth/auth.actions.ts

@ -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

@ -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});
}

2
ui-ngx/src/app/core/auth/auth.models.ts

@ -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 {

22
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -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;
}
}
};

36
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -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;
};

15
ui-ngx/src/app/core/auth/auth.service.ts

@ -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> {

1
ui-ngx/src/app/core/auth/public-api.ts

@ -15,6 +15,7 @@
///
export * from './auth.actions';
export * from './auth.effects';
export * from './auth.models';
export * from './auth.reducer';
export * from './auth.selectors';

2
ui-ngx/src/app/core/core.state.ts

@ -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
];

1
ui-ngx/src/app/core/http/public-api.ts

@ -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

@ -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

@ -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();
}
}

1
ui-ngx/src/app/core/services/menu.models.ts

@ -26,6 +26,7 @@ export interface MenuSection extends HasUUID{
isMdiIcon?: boolean;
height?: string;
pages?: Array<MenuSection>;
opened?: boolean;
}
export interface HomeSection {

92
ui-ngx/src/app/core/services/menu.service.ts

@ -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,47 +153,51 @@ export class MenuService {
},
{
id: guid(),
name: 'admin.sms-provider',
name: 'admin.notifications',
type: 'link',
path: '/settings/sms-provider',
icon: 'sms'
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
},
{
id: guid(),
name: 'admin.security-settings',
name: 'admin.queues',
type: 'link',
path: '/settings/security-settings',
icon: 'security'
path: '/settings/queues',
icon: 'swap_calls'
},
]
},
{
id: guid(),
name: 'security.security',
type: 'link',
path: '/security-settings',
icon: 'security',
pages: [
{
id: guid(),
name: 'admin.oauth2.oauth2',
name: 'admin.general',
type: 'link',
path: '/settings/oauth2',
icon: 'security'
path: '/security-settings/general',
icon: 'settings_applications'
},
{
id: guid(),
name: 'admin.2fa.2fa',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/settings/2fa',
icon: 'mdi:two-factor-authentication',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account',
isMdiIcon: true
},
{
id: guid(),
name: 'resource.resources-library',
type: 'link',
path: '/settings/resources-library',
icon: 'folder'
},
{
id: guid(),
name: 'admin.queues',
name: 'admin.2fa.2fa',
type: 'link',
path: '/settings/queues',
icon: 'swap_calls'
},
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
}
]
}
);

4
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -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">

8
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html

@ -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>

6
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts

@ -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();
}
);
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -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,

6
ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts

@ -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();
}
}
}

56
ui-ngx/src/app/modules/home/components/route-tabs.component.scss

@ -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%;
}
}

35
ui-ngx/src/app/modules/home/components/router-tabs.component.html

@ -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>

65
ui-ngx/src/app/modules/home/components/router-tabs.component.ts

@ -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);
}
}

6
ui-ngx/src/app/modules/home/home.component.html

@ -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>

7
ui-ngx/src/app/modules/home/home.component.ts

@ -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;

6
ui-ngx/src/app/modules/home/menu/menu-link.component.html

@ -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>

9
ui-ngx/src/app/modules/home/menu/menu-toggle.component.html

@ -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
ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts

@ -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;
}

346
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -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> {
@ -50,12 +60,12 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
const routes: Routes = [
{
path: 'settings',
path: 'resources',
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.system-settings',
icon: 'settings'
label: 'admin.resources',
icon: 'folder'
}
},
children: [
@ -64,99 +74,94 @@ const routes: Routes = [
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: {
SYS_ADMIN: '/settings/general',
TENANT_ADMIN: '/settings/home'
}
}
},
{
path: 'general',
component: GeneralSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.general-settings',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
}
redirectTo: '/resources/widgets-bundles'
}
},
{
path: 'outgoing-mail',
component: MailServerComponent,
canDeactivate: [ConfirmOnExitGuard],
path: 'widgets-bundles',
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.outgoing-mail-settings',
breadcrumb: {
label: 'admin.outgoing-mail',
icon: 'mail'
}
}
},
{
path: 'sms-provider',
component: SmsProviderComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.sms-provider-settings',
breadcrumb: {
label: 'admin.sms-provider',
icon: 'sms'
}
}
},
{
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'
label: 'widgets-bundle.widgets-bundles',
icon: 'now_widgets'
}
},
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'
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: 'folder'
icon: 'mdi:rhombus-split'
}
},
children: [
@ -178,7 +183,7 @@ const routes: Routes = [
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'folder'
icon: 'mdi:rhombus-split'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library'
@ -188,6 +193,69 @@ const routes: Routes = [
}
}
]
}
]
},
{
path: 'settings',
component: RouterTabsComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.system-settings',
icon: 'settings'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: {
SYS_ADMIN: '/settings/general',
TENANT_ADMIN: '/settings/home'
}
}
},
{
path: 'general',
component: GeneralSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.general-settings',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
}
}
},
{
path: 'outgoing-mail',
component: MailServerComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.outgoing-mail-settings',
breadcrumb: {
label: 'admin.outgoing-mail',
icon: 'mail'
}
}
},
{
path: 'notifications',
component: SmsProviderComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.notifications-settings',
breadcrumb: {
label: 'admin.notifications',
icon: 'mdi:message-badge'
}
}
},
{
path: 'queues',
@ -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 { }

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -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);
}

2
ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html

@ -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>

2
ui-ngx/src/app/modules/home/pages/home-pages.models.ts

@ -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;
});

5
ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html

@ -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>

100
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -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 { }

2
ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts

@ -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) {

33
ui-ngx/src/app/shared/components/breadcrumb.component.html

@ -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>

23
ui-ngx/src/app/shared/components/breadcrumb.component.ts

@ -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';

1
ui-ngx/src/app/shared/models/public-api.ts

@ -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

@ -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: []
};

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -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",

1
ui-ngx/src/assets/mdi.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.3 MiB

5
ui-ngx/yarn.lock

@ -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…
Cancel
Save