- - - - - - -
- - - -
- - - @if (fullscreenEnabled) { - + + + - {{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }} + class="mat-toolbar-tools flex-1"> + +
+ + + +
+ + + @if (fullscreenEnabled) { + + } + + @if (forceFullscreen) { + + } +
+ @if (!hideLoadingBar && (isLoading$ | async)) { + + } - - - @if (!hideLoadingBar && (isLoading$ | async)) { - - }
diff --git a/ui-ngx/src/app/modules/home/home.component.scss b/ui-ngx/src/app/modules/home/home.component.scss index 131b074aa9..0fe46ac42c 100644 --- a/ui-ngx/src/app/modules/home/home.component.scss +++ b/ui-ngx/src/app/modules/home/home.component.scss @@ -32,7 +32,7 @@ transition: opacity $sidenav-anim-timing; } } - mat-sidenav-content.tb-site-content { + mat-sidenav-content.tb-site-content:not(.tb-force-fullscreen) { margin-left: 250px !important; transition: margin-left $sidenav-anim-timing; } diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index b3f48ed6da..83b6652853 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -18,14 +18,14 @@ import { AfterViewInit, Component, computed, - ElementRef, + ElementRef, EventEmitter, Inject, OnDestroy, OnInit, signal, ViewChild } from '@angular/core'; -import { skip, startWith, Subject } from 'rxjs'; +import { skip, startWith, Subject, Subscription } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; @@ -44,6 +44,7 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { FormBuilder } from '@angular/forms'; import { isDefinedAndNotNull } from '@core/utils'; import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; +import { isMainToolbarComponent } from '@home/models/main-toolbar.models'; @Component({ selector: 'tb-home', @@ -85,6 +86,10 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni hideLoadingBar = false; + hideMainToolbar = false; + + private toggleSideBarSubscription: Subscription; + private destroy$ = new Subject(); constructor(protected store: Store, @@ -125,6 +130,9 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni } ngOnDestroy() { + if (this.toggleSideBarSubscription) { + this.toggleSideBarSubscription.unsubscribe(); + } this.destroy$.next(); this.destroy$.complete(); } @@ -197,6 +205,22 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni this.searchEnabled = false; this.searchableComponent = null; } + + if (this.toggleSideBarSubscription) { + this.toggleSideBarSubscription.unsubscribe(); + this.toggleSideBarSubscription = null; + } + + this.hideMainToolbar = false; + + if (isMainToolbarComponent(this.activeComponent)) { + this.hideMainToolbar = this.activeComponent.hideMainToolbar; + this.toggleSideBarSubscription = this.activeComponent.toggleSideBar.subscribe( + () => { + this.sidenav.toggle(); + } + ); + } } displaySearchMode(): boolean { diff --git a/ui-ngx/src/app/modules/home/home.module.ts b/ui-ngx/src/app/modules/home/home.module.ts index 08dbf42104..293820941d 100644 --- a/ui-ngx/src/app/modules/home/home.module.ts +++ b/ui-ngx/src/app/modules/home/home.module.ts @@ -23,9 +23,8 @@ import { SharedModule } from '@app/shared/shared.module'; import { MenuLinkComponent } from '@modules/home/menu/menu-link.component'; import { MenuToggleComponent } from '@modules/home/menu/menu-toggle.component'; import { SideMenuComponent } from '@modules/home/menu/side-menu.component'; -import { GithubBadgeComponent } from '@home/components/github-badge/github-badge.component'; -import { NotificationBellComponent } from '@home/components/notification/notification-bell.component'; -import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component'; +import { NotificationBellModule } from '@home/components/notification/notification-bell.module'; +import { GithubBadgeModule } from '@home/components/github-badge/github-badge.module'; @NgModule({ declarations: @@ -33,14 +32,13 @@ import { ShowNotificationPopoverComponent } from '@home/components/notification/ HomeComponent, MenuLinkComponent, MenuToggleComponent, - SideMenuComponent, - GithubBadgeComponent, - NotificationBellComponent, - ShowNotificationPopoverComponent + SideMenuComponent ], imports: [ CommonModule, SharedModule, + NotificationBellModule, + GithubBadgeModule, HomeRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/home/models/main-toolbar.models.ts b/ui-ngx/src/app/modules/home/models/main-toolbar.models.ts new file mode 100644 index 0000000000..1b7218b57c --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/main-toolbar.models.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2026 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 { EventEmitter } from '@angular/core'; +import { isObservable } from 'rxjs'; + +export interface MainToolbarComponent { + hideMainToolbar: boolean; + toggleSideBar: EventEmitter; +} + +export const isMainToolbarComponent = (cmp: any): cmp is MainToolbarComponent => { + const mainToolbarCmp = (cmp as MainToolbarComponent); + return isObservable(mainToolbarCmp.toggleSideBar); +}; diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index 55d33c833b..0c2dc6b3f8 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -26,7 +26,7 @@ import { AssetsTableConfigResolver } from '../asset/assets-table-config.resolver import { DashboardsTableConfigResolver } from '@modules/home/pages/dashboard/dashboards-table-config.resolver'; import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; -import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; +import { DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.resolver'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @@ -254,7 +254,6 @@ const routes: Routes = [ canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { - labelFunction: dashboardBreadcumbLabelFunction, icon: 'dashboard' } as BreadCrumbConfig, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts index 3501141558..3f1392154c 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -21,7 +21,7 @@ import { EntitiesTableComponent } from '../../components/entity/entities-table.c import { Authority } from '@shared/models/authority.enum'; import { DashboardsTableConfigResolver } from './dashboards-table-config.resolver'; import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; -import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { mergeMap, Observable, of } from 'rxjs'; import { Dashboard } from '@app/shared/models/dashboard.models'; import { DashboardService } from '@core/http/dashboard.service'; @@ -59,9 +59,6 @@ export class DashboardResolver { } } -export const dashboardBreadcumbLabelFunction: BreadCrumbLabelFunction - = ((route, translate, component) => component.dashboard.title); - const routes: Routes = [ { path: 'dashboards', @@ -89,7 +86,6 @@ const routes: Routes = [ canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { - labelFunction: dashboardBreadcumbLabelFunction, icon: 'dashboard' } as BreadCrumbConfig, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index 49f5b2e988..eacf54cbff 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -25,7 +25,7 @@ import { EntityViewsTableConfigResolver } from '@home/pages/entity-view/entity-v import { DashboardsTableConfigResolver } from '@home/pages/dashboard/dashboards-table-config.resolver'; import { RuleChainsTableConfigResolver } from '@home/pages/rulechain/rulechains-table-config.resolver'; import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; -import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; +import { DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @@ -243,7 +243,6 @@ const routes: Routes = [ canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { - labelFunction: dashboardBreadcumbLabelFunction, icon: 'dashboard' } as BreadCrumbConfig, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 52b757bfe0..ad257af29c 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -88,9 +88,11 @@ export const widgetsBundleWidgetsBreadcumbLabelFunction: BreadCrumbLabelFunction route.data.widgetsBundle.title); export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction = - ((route, translate, component) => - component?.widget?.widgetName ? - (component.widget.widgetName + (component.widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : ''); + ((route, translate, component) => { + const widget = component?.widget ?? route.data?.widgetEditorData?.widget; + return widget?.widgetName ? + (widget.widgetName + (widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : ''; + }); const widgetEditorRouter: Routes = [ { diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 00ede9d0dd..6730d5a321 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -16,33 +16,64 @@ -->
- @if (lastBreadcrumb$ | async; as breadcrumb) { -

+ @if (componentBreadcrumbsTpl && (lastBreadcrumb$ | async) === null) { +
+ *ngTemplateOutlet="componentBreadcrumbsTpl"> -

- } - @for (breadcrumb of breadcrumbs$ | async; track trackByBreadcrumbs; let isLast = $last; let i = $index; let total = $count;) { - - @switch (isLast) { - @case (false) { - - - - - } - @case (true) { - - - - +
+ } @else { + @if (lastBreadcrumb$ | async; as breadcrumb) { + @if (componentBreadcrumbsTpl) { +
+ + +
+ } @else { +

+ + +

+ } + } +
+ @for (breadcrumb of breadcrumbs$ | async; track breadcrumb.id; let isLast = $last; let i = $index;) { + @switch (isLast) { + @case (false) { + @if (i > 0) { + > + } + + + + + + + } + @case (true) { + @if (componentBreadcrumbsTpl) { + + + + + } @else { + @if (i > 0) { + > + } + + + + + } + } } } - > - +
}
diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.scss b/ui-ngx/src/app/shared/components/breadcrumb.component.scss index 3059cf5c43..3d64af65ce 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.scss +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.scss @@ -14,6 +14,7 @@ * limitations under the License. */ :host { + pointer-events: auto; min-width: 0; .tb-breadcrumb { diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 9067d5c04e..8ec092f803 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -14,18 +14,19 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; -import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; -import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; -import { distinctUntilChanged, filter, first, map, switchMap } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; -import { guid } from '@core/utils'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, OnDestroy, + OnInit, + TemplateRef +} from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { BreadCrumb } from './breadcrumb'; import { BroadcastService } from '@core/services/broadcast.service'; -import { ActiveComponentService } from '@core/services/active-component.service'; import { UtilsService } from '@core/services/utils.service'; -import { MenuSection, menuSectionMap } from '@core/services/menu.models'; -import { MenuService } from '@core/services/menu.service'; +import { BreadcrumbService } from '@core/services/breadcrumb.service'; @Component({ selector: 'tb-breadcrumb', @@ -36,116 +37,29 @@ import { MenuService } from '@core/services/menu.service'; }) export class BreadcrumbComponent implements OnInit, OnDestroy { - activeComponentValue: any; - updateBreadcrumbsSubscription: Subscription = null; + @Input() + componentBreadcrumbsTpl?: TemplateRef; - setActiveComponent(activeComponent: any) { - if (this.updateBreadcrumbsSubscription) { - this.updateBreadcrumbsSubscription.unsubscribe(); - this.updateBreadcrumbsSubscription = null; - } - this.activeComponentValue = activeComponent; - if (this.activeComponentValue && this.activeComponentValue.updateBreadcrumbs) { - this.updateBreadcrumbsSubscription = this.activeComponentValue.updateBreadcrumbs.subscribe(() => { - this.menuService.availableMenuSections().pipe(first()).subscribe((sections) => { - this.breadcrumbs$.next(this.buildBreadCrumbs(this.activatedRoute.snapshot, sections)); - }); - }); - } - } - - breadcrumbs$: Subject> = new BehaviorSubject>([]); + breadcrumbs$: Observable = this.breadcrumbService.breadcrumbs$; + lastBreadcrumb$: Observable = this.breadcrumbService.lastBreadcrumb$; - routerEventsSubscription = this.router.events.pipe( - filter((event) => event instanceof NavigationEnd ), - distinctUntilChanged(), - switchMap(() => this.menuService.availableMenuSections().pipe(first())), - map( (sections) => this.buildBreadCrumbs(this.activatedRoute.snapshot, sections) ) - ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); + private updateBreadcrumbsSubscription: Subscription; - activeComponentSubscription = this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp)); - - lastBreadcrumb$ = this.breadcrumbs$.pipe( - map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1]) - ); - - constructor(private router: Router, - private activatedRoute: ActivatedRoute, - private broadcast: BroadcastService, - private activeComponentService: ActiveComponentService, + constructor(private broadcast: BroadcastService, + private breadcrumbService: BreadcrumbService, private cd: ChangeDetectorRef, - private translate: TranslateService, - private menuService: MenuService, public utils: UtilsService) { } ngOnInit(): void { - this.broadcast.on('updateBreadcrumb', () => { + this.updateBreadcrumbsSubscription = 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) { - let child = route; - while (child.firstChild !== null) { - child = child.firstChild; - } - return child; - } - - buildBreadCrumbs(route: ActivatedRouteSnapshot, availableMenuSections: MenuSection[], - breadcrumbs: Array = [], - lastChild?: ActivatedRouteSnapshot): Array { - if (!lastChild) { - lastChild = this.lastChild(route); - } - let newBreadcrumbs = breadcrumbs; - if (route.routeConfig && route.routeConfig.data) { - const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; - if (breadcrumbConfig && !breadcrumbConfig.skip) { - let labelFunction: () => string; - let section: MenuSection = null; - if (breadcrumbConfig.menuId) { - section = availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId); - if (!section) { - section = menuSectionMap.get(breadcrumbConfig.menuId); - } - } - const label = section?.name || breadcrumbConfig.label || 'home.home'; - const customTranslate = section?.customTranslate || false; - if (breadcrumbConfig.labelFunction) { - labelFunction = () => this.activeComponentValue ? - breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data) : label; - } - const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; - const breadcrumb = { - id: guid(), - label, - customTranslate, - labelFunction, - link, - queryParams: null - }; - newBreadcrumbs = [...breadcrumbs, breadcrumb]; - } - } - if (route.firstChild) { - return this.buildBreadCrumbs(route.firstChild, availableMenuSections, newBreadcrumbs, lastChild); + if (this.updateBreadcrumbsSubscription) { + this.updateBreadcrumbsSubscription.unsubscribe(); } - return newBreadcrumbs; - } - - trackByBreadcrumbs(_index: number, breadcrumb: BreadCrumb){ - return breadcrumb.id; } } diff --git a/ui-ngx/src/app/shared/components/user-menu.component.scss b/ui-ngx/src/app/shared/components/user-menu.component.scss index 2edda733d2..f3060b8585 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.scss +++ b/ui-ngx/src/app/shared/components/user-menu.component.scss @@ -16,6 +16,15 @@ @import "constants"; +.tb-primary-toolbar, .tb-dashboard-action-panel { + tb-user-menu { + width: auto; + button.mat-mdc-button:not(.mat-mdc-icon-button).tb-user-menu { + padding: 12px 16px; + } + } +} + tb-user-menu { width: 100%; diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index cb6e47a637..820acbc9de 100644 --- a/ui-ngx/src/theme.scss +++ b/ui-ngx/src/theme.scss @@ -134,8 +134,8 @@ $tb-dark-theme: map_merge($tb-dark-theme, (_mat-system: $tb-dark-theme-system)); mat-fab-toolbar { .mat-fab-toolbar-background { - background: mat.m2-get-color-from-palette($background, app-bar); - color: mat.m2-get-color-from-palette($foreground, text); + background: var(--mat-toolbar-container-background-color, var(--mat-sys-surface)); + color: var(--mat-toolbar-container-text-color, var(--mat-sys-on-surface)); } &.mat-primary { .mat-fab-toolbar-background {