Browse Source

Merge pull request #15888 from thingsboard/feature/new-menu

Front-end sidebar/toolbar redesign
pull/15903/head
Igor Kulikov 4 weeks ago
committed by GitHub
parent
commit
ca9f1463d6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 149
      ui-ngx/src/app/core/services/breadcrumb.service.ts
  2. 70
      ui-ngx/src/app/core/services/home.service.ts
  3. 471
      ui-ngx/src/app/core/services/menu.models.ts
  4. 37
      ui-ngx/src/app/core/services/menu.service.ts
  5. 2
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html
  6. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  8. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html
  9. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  10. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
  11. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  12. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  13. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  15. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  16. 2
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html
  17. 2
      ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html
  18. 2
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html
  19. 2
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.html
  20. 2
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  21. 2
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html
  22. 2
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html
  23. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  24. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  25. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  26. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  27. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html
  28. 92
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  29. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  30. 20
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  31. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  32. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html
  33. 9
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html
  34. 32
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  35. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  36. 29
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  37. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html
  38. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html
  39. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.html
  40. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html
  41. 19
      ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html
  42. 44
      ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.scss
  43. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts
  44. 42
      ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html
  45. 46
      ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.scss
  46. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html
  48. 1
      ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  50. 2
      ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html
  51. 2
      ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html
  52. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  53. 1
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  54. 2
      ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.html
  55. 2
      ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts
  56. 2
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  57. 2
      ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html
  58. 2
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  59. 2
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  60. 2
      ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html
  61. 2
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  62. 2
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  63. 2
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  64. 2
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.scss
  65. 31
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts
  66. 35
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.module.ts
  67. 6
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  68. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/notification/notification-bell.component.html
  70. 38
      ui-ngx/src/app/modules/home/components/notification/notification-bell.module.ts
  71. 2
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  72. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  73. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
  75. 2
      ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html
  76. 10
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html
  77. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html
  78. 10
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  79. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html
  80. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html
  81. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html
  82. 2
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html
  83. 8
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html
  84. 2
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html
  85. 2
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html
  86. 2
      ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html
  87. 8
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
  88. 2
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  89. 5
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss
  90. 6
      ui-ngx/src/app/modules/home/components/router-tabs.component.ts
  91. 2
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html
  92. 2
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  93. 4
      ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html
  94. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html
  95. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html
  96. 16
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss
  97. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html
  98. 15
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss
  99. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html
  100. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html

149
ui-ngx/src/app/core/services/breadcrumb.service.ts

@ -0,0 +1,149 @@
///
/// 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 { Injectable } from '@angular/core';
import { BehaviorSubject, merge, shareReplay, Subject, Subscription } from 'rxjs';
import { BreadCrumb, BreadCrumbConfig } from '@shared/components/breadcrumb';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { MenuService } from '@core/services/menu.service';
import { distinctUntilChanged, filter, first, map, switchMap } from 'rxjs/operators';
import { MenuSection, menuSectionMap } from '@core/services/menu.models';
import { guid } from '@core/utils';
import { ActiveComponentService } from '@core/services/active-component.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@Injectable({
providedIn: 'root'
})
export class BreadcrumbService {
private updateBreadcrumbsSubscription: Subscription = null;
private breadcrumbsSubject: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>([]);
private activeComponent: any;
get breadcrumbs$() {
return this.breadcrumbsSubject.asObservable().pipe(shareReplay(1));
}
get lastBreadcrumb$() {
return this.breadcrumbs$.pipe<BreadCrumb>(
map( (breadcrumbs: BreadCrumb[]) => breadcrumbs.length ? breadcrumbs[breadcrumbs.length - 1] : null)
);
}
constructor(private router: Router,
private store: Store<AppState>,
private activatedRoute: ActivatedRoute,
private translate: TranslateService,
private menuService: MenuService,
private activeComponentService: ActiveComponentService) {
merge(this.router.events.pipe(
filter((event) => event instanceof NavigationEnd ),
distinctUntilChanged()), this.menuService.availableMenuSections()).pipe(
switchMap(() => this.menuService.availableMenuSections().pipe(first())),
map( (sections: MenuSection[]) => this.buildBreadCrumbs(this.activatedRoute.snapshot, sections) )
).subscribe((breadcrumbs: BreadCrumb[]) => this.breadcrumbsSubject.next(breadcrumbs) );
this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp));
}
private setActiveComponent(activeComponent: any) {
if (this.updateBreadcrumbsSubscription) {
this.updateBreadcrumbsSubscription.unsubscribe();
this.updateBreadcrumbsSubscription = null;
}
this.activeComponent = activeComponent;
if (this.activeComponent) {
if (this.activeComponent.updateBreadcrumbs) {
this.updateBreadcrumbsSubscription = this.activeComponent.updateBreadcrumbs.subscribe(() => {
this.menuService.availableMenuSections().pipe(first()).subscribe((sections: MenuSection[]) => {
const breadcrumbs = this.buildBreadCrumbs(this.activatedRoute.snapshot, sections);
this.breadcrumbsSubject.next(breadcrumbs);
});
});
}
}
}
private buildBreadCrumbs(route: ActivatedRouteSnapshot, availableMenuSections: MenuSection[],
breadcrumbs: Array<BreadCrumb> = [],
lastChild?: ActivatedRouteSnapshot): Array<BreadCrumb> {
if (!lastChild) {
lastChild = this.lastChild(route);
}
let newBreadcrumbs = breadcrumbs;
if (route.routeConfig && route.routeConfig.data) {
const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig<any>;
if (breadcrumbConfig && !breadcrumbConfig.skip) {
let labelFunction: () => string;
let section: MenuSection = null;
let menuId = breadcrumbConfig.menuId;
if (!menuId && breadcrumbConfig.menuIdByAuthority) {
const authority = getCurrentAuthUser(this.store).authority;
menuId = breadcrumbConfig.menuIdByAuthority[authority];
}
if (menuId) {
section = availableMenuSections.find(menu => menu.id === menuId);
if (!section) {
section = menuSectionMap.get(menuId);
}
}
const label = section?.name || breadcrumbConfig.label || 'home.home';
const customTranslate = section?.customTranslate || false;
if (breadcrumbConfig.labelFunction) {
labelFunction = () => {
if (this.activeComponent) {
try {
return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponent, lastChild.data);
} catch {
return label;
}
} else {
return 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);
}
return newBreadcrumbs;
}
private lastChild(route: ActivatedRouteSnapshot) {
let child = route;
while (child.firstChild !== null) {
child = child.firstChild;
}
return child;
}
}

70
ui-ngx/src/app/core/services/home.service.ts

@ -0,0 +1,70 @@
///
/// 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, Injectable } from '@angular/core';
import { ActiveComponentService } from '@core/services/active-component.service';
import { BehaviorSubject, Observable, shareReplay, Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { isDefinedAndNotNull } from '@core/utils';
import { PageComponent } from '@shared/components/page.component';
@Injectable({
providedIn: 'root'
})
export class HomeService {
private hideMainToolbarSubject: Subject<boolean> = new BehaviorSubject<boolean>(false);
private hideLoadingBarSubject: Subject<boolean> = new BehaviorSubject<boolean>(false);
get hideMainToolbar$() {
return this.hideMainToolbarSubject.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
}
get hideLoadingBar$() {
return this.hideLoadingBarSubject.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
}
toggleSideBar = new EventEmitter<void>();
constructor(private activeComponentService: ActiveComponentService) {
this.activeComponentService.onActiveComponentChanged().subscribe(activeComponent => {
Promise.resolve().then(() => {
this.activeComponentChanged(activeComponent);
});
});
}
public setHideMainToolbar(hide: boolean): void {
Promise.resolve().then(() => {
this.hideMainToolbarSubject.next(hide);
});
}
private activeComponentChanged(activeComponent: any) {
this.hideMainToolbarSubject.next(false);
let hideLoadingBar = false;
if (activeComponent && activeComponent instanceof RouterTabsComponent
&& isDefinedAndNotNull(activeComponent.activatedRoute?.snapshot?.data?.showMainLoadingBar)) {
hideLoadingBar = !activeComponent.activatedRoute.snapshot.data.showMainLoadingBar;
} else if (activeComponent && activeComponent instanceof PageComponent
&& isDefinedAndNotNull(activeComponent?.showMainLoadingBar)) {
hideLoadingBar = !activeComponent.showMainLoadingBar;
}
this.hideLoadingBarSubject.next(hideLoadingBar);
}
}

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

@ -18,7 +18,7 @@ import { AuthState } from '@core/auth/auth.models';
import { Authority } from '@shared/models/authority.enum';
import { deepClone } from '@core/utils';
export declare type MenuSectionType = 'link' | 'toggle';
export declare type MenuSectionType = 'link' | 'toggle' | 'divider';
export interface MenuSection {
id: MenuId | string;
@ -32,6 +32,7 @@ export interface MenuSection {
rootOnly?: boolean;
isNew?: boolean;
customTranslate?: boolean;
active?: boolean;
}
export interface MenuReference {
@ -39,11 +40,6 @@ export interface MenuReference {
pages?: Array<MenuReference>;
}
export interface HomeSectionReference {
name: string;
places: Array<MenuId>;
}
export interface HomeSection {
name: string;
places: Array<MenuSection>;
@ -52,6 +48,7 @@ export interface HomeSection {
export enum MenuId {
home = 'home',
tenants = 'tenants',
tenants_section = 'tenants_section',
tenant_profiles = 'tenant_profiles',
resources = 'resources',
widget_library = 'widget_library',
@ -71,6 +68,8 @@ export enum MenuId {
mobile_apps = 'mobile_apps',
mobile_bundles = 'mobile_bundles',
mobile_qr_code_widget = 'mobile_qr_code_widget',
platform = 'platform',
platform_section = 'platform_section',
settings = 'settings',
general = 'general',
mail_server = 'mail_server',
@ -86,6 +85,7 @@ export enum MenuId {
domains = 'domains',
clients = 'clients',
audit_log = 'audit_log',
monitor = 'monitor',
alarms_center = 'alarms_center',
alarms = 'alarms',
alarm_rules = 'alarm_rules',
@ -98,7 +98,9 @@ export enum MenuId {
profiles = 'profiles',
device_profiles = 'device_profiles',
asset_profiles = 'asset_profiles',
customers_and_users = 'customers_and_users',
customers = 'customers',
data_processing = 'data_processing',
calculated_fields = 'calculated_fields',
rule_chains = 'rule_chains',
edge_management = 'edge_management',
@ -111,7 +113,8 @@ export enum MenuId {
api_usage = 'api_usage',
trendz_settings = 'trendz_settings',
ai_models = 'ai_models',
iot_hub = 'iot_hub'
iot_hub = 'iot_hub',
divider = 'divider'
}
declare type MenuFilter = (authState: AuthState) => boolean;
@ -124,7 +127,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
icon: 'mdi:home-outline'
}
],
[
MenuId.tenants_section,
{
id: MenuId.tenants_section,
name: 'tenant.tenants',
type: 'toggle',
path: '/tenants',
icon: 'mdi:account-supervisor-outline'
}
],
[
@ -134,7 +147,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'tenant.tenants',
type: 'link',
path: '/tenants',
icon: 'supervisor_account'
icon: 'mdi:account-supervisor-outline'
}
],
[
@ -144,7 +157,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box'
icon: 'mdi:alpha-t-box-outline'
}
],
[
@ -154,17 +167,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder'
icon: 'mdi:folder-outline'
}
],
[
MenuId.widget_library,
{
id: MenuId.widget_library,
name: 'widget.widget-library',
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library',
icon: 'now_widgets'
icon: 'mdi:widgets-outline'
}
],
[
@ -174,7 +187,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
icon: 'mdi:widgets-outline'
}
],
[
@ -184,14 +197,14 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
icon: 'mdi:widgets-outline'
}
],
[
MenuId.images,
{
id: MenuId.images,
name: 'image.gallery',
name: 'image.images',
type: 'link',
path: '/resources/images',
icon: 'filter'
@ -211,17 +224,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.resources_library,
{
id: MenuId.resources_library,
name: 'resource.resources-library',
name: 'resource.files',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split'
icon: 'mdi:rhombus-split-outline'
}
],
[
MenuId.javascript_library,
{
id: MenuId.javascript_library,
name: 'javascript.javascript-library',
name: 'javascript.scripts',
type: 'link',
path: '/resources/javascript-library',
icon: 'mdi:language-javascript'
@ -231,10 +244,10 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.notifications_center,
{
id: MenuId.notifications_center,
name: 'notification.notification-center',
name: 'notification.notifications',
type: 'link',
path: '/notification',
icon: 'mdi:message-badge'
icon: 'mdi:message-badge-outline'
}
],
[
@ -267,7 +280,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'notification.notification-recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
icon: 'mdi:contacts-outline'
}
],
[
@ -289,7 +302,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog'
icon: 'mdi:message-cog-outline'
}
],
[
@ -306,7 +319,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.mobile_center,
{
id: MenuId.mobile_center,
name: 'mobile.mobile-center',
name: 'mobile.mobile-apps',
type: 'link',
path: '/mobile-center',
icon: 'smartphone'
@ -343,6 +356,26 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
icon: 'qr_code'
}
],
[
MenuId.platform,
{
id: MenuId.platform,
name: 'admin.platform',
type: 'link',
path: '/settings',
icon: 'miscellaneous_services'
}
],
[
MenuId.platform_section,
{
id: MenuId.platform_section,
name: 'admin.platform',
type: 'toggle',
path: '/platform',
icon: 'miscellaneous_services'
}
],
[
MenuId.settings,
{
@ -350,7 +383,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings'
icon: 'mdi:cog-outline'
}
],
[
@ -361,7 +394,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'admin.general-settings',
type: 'link',
path: '/settings/general',
icon: 'settings_applications'
icon: 'mdi:cog-outline'
}
],
[
@ -371,7 +404,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'admin.outgoing-mail',
type: 'link',
path: '/settings/outgoing-mail',
icon: 'mail'
icon: 'mail_outline'
}
],
[
@ -382,7 +415,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'admin.home-settings',
type: 'link',
path: '/settings/home',
icon: 'settings_applications'
icon: 'mdi:cog-outline'
}
],
[
@ -393,7 +426,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge'
icon: 'mdi:message-badge-outline'
}
],
[
@ -446,7 +479,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
fullName: 'security.general-settings',
type: 'link',
path: '/security-settings/general',
icon: 'settings_applications'
icon: 'mdi:cog-outline'
}
],
[
@ -466,7 +499,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account'
icon: 'mdi:shield-account-outline'
}
],
[
@ -499,6 +532,16 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
icon: 'track_changes'
}
],
[
MenuId.monitor,
{
id: MenuId.monitor,
name: 'monitor.monitor',
type: 'toggle',
path: '/monitor',
icon: 'mdi:view-dashboard-outline'
}
],
[
MenuId.alarms_center,
{
@ -536,17 +579,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
icon: 'mdi:view-dashboard-outline'
}
],
[
MenuId.entities,
{
id: MenuId.entities,
name: 'entity.entities',
name: 'entity.devices-and-assets',
type: 'toggle',
path: '/entities',
icon: 'category'
icon: 'mdi:shape-outline'
}
],
[
@ -576,7 +619,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
icon: 'mdi:view-quilt-outline'
}
],
[
@ -606,7 +649,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box'
icon: 'mdi:alpha-d-box-outline'
}
],
[
@ -616,7 +659,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box'
icon: 'mdi:alpha-a-box-outline'
}
],
[
MenuId.customers_and_users,
{
id: MenuId.customers_and_users,
name: 'customer.customers-and-users',
type: 'link',
path: '/customers',
icon: 'mdi:account-multiple-outline'
}
],
[
@ -626,7 +679,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'customer.customers',
type: 'link',
path: '/customers',
icon: 'supervisor_account'
icon: 'mdi:account-supervisor-outline'
}
],
[
MenuId.data_processing,
{
id: MenuId.data_processing,
name: 'entity.data-processing',
type: 'toggle',
path: '/dataProcessing',
icon: 'settings_ethernet',
}
],
[
@ -729,7 +792,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'api-usage.api-usage',
type: 'link',
path: '/usage',
icon: 'insert_chart'
icon: 'insert_chart_outlined'
}
],
[
@ -750,9 +813,19 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'iot-hub.iot-hub',
type: 'link',
path: '/iot-hub',
icon: 'hub',
icon: 'mdi:hub-outline',
isNew: true
}
],
[
MenuId.divider,
{
id: MenuId.divider,
name: '',
type: 'divider',
path: 'divider',
icon: ''
}
]
]);
@ -773,22 +846,11 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
Authority.SYS_ADMIN,
[
{id: MenuId.home},
{id: MenuId.tenants},
{id: MenuId.tenant_profiles},
{
id: MenuId.resources,
id: MenuId.tenants_section,
pages: [
{
id: MenuId.widget_library,
pages: [
{id: MenuId.widget_types},
{id: MenuId.widgets_bundles}
]
},
{id: MenuId.images},
{id: MenuId.scada_symbols},
{id: MenuId.javascript_library},
{id: MenuId.resources_library}
{id: MenuId.tenants},
{id: MenuId.tenant_profiles},
]
},
{
@ -802,20 +864,19 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
]
},
{
id: MenuId.mobile_center,
pages: [
{id: MenuId.mobile_bundles},
{id: MenuId.mobile_apps},
{id: MenuId.mobile_qr_code_widget}
]
},
{
id: MenuId.settings,
id: MenuId.resources,
pages: [
{id: MenuId.general},
{id: MenuId.mail_server},
{id: MenuId.notification_settings},
{id: MenuId.queues}
{
id: MenuId.widget_library,
pages: [
{id: MenuId.widget_types},
{id: MenuId.widgets_bundles}
]
},
{id: MenuId.images},
{id: MenuId.scada_symbols},
{id: MenuId.javascript_library},
{id: MenuId.resources_library}
]
},
{
@ -832,6 +893,23 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
},
{id: MenuId.audit_log}
]
},
{
id: MenuId.platform,
pages: [
{id: MenuId.general},
{id: MenuId.mail_server},
{id: MenuId.notification_settings},
{id: MenuId.queues}
]
},
{
id: MenuId.mobile_center,
pages: [
{id: MenuId.mobile_bundles},
{id: MenuId.mobile_apps},
{id: MenuId.mobile_qr_code_widget}
]
}
]
],
@ -839,46 +917,49 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
Authority.TENANT_ADMIN,
[
{id: MenuId.home},
{id: MenuId.iot_hub},
{id: MenuId.divider},
{
id: MenuId.alarms_center,
id: MenuId.monitor,
pages: [
{id: MenuId.alarms},
{id: MenuId.alarm_rules}
{id: MenuId.dashboards},
{
id: MenuId.alarms_center,
pages: [
{id: MenuId.alarms},
{id: MenuId.alarm_rules}
]
},
{
id: MenuId.notifications_center,
pages: [
{id: MenuId.notification_inbox},
{id: MenuId.notification_sent},
{id: MenuId.notification_recipients},
{id: MenuId.notification_templates},
{id: MenuId.notification_rules}
]
}
]
},
{id: MenuId.dashboards},
{id: MenuId.iot_hub},
{
id: MenuId.entities,
pages: [
{id: MenuId.devices},
{id: MenuId.gateways},
{id: MenuId.assets},
{id: MenuId.entity_views},
{id: MenuId.gateways}
]
},
{
id: MenuId.profiles,
pages: [
{id: MenuId.device_profiles},
{id: MenuId.asset_profiles}
]
},
{id: MenuId.customers},
{id: MenuId.calculated_fields},
{id: MenuId.rule_chains},
{
id: MenuId.edge_management,
pages: [
{id: MenuId.edges},
{id: MenuId.rulechain_templates}
{id: MenuId.asset_profiles},
{id: MenuId.entity_views},
{id: MenuId.otaUpdates}
]
},
{id: MenuId.customers_and_users},
{
id: MenuId.features,
id: MenuId.data_processing,
pages: [
{id: MenuId.otaUpdates},
{id: MenuId.version_control}
{id: MenuId.calculated_fields},
{id: MenuId.rule_chains}
]
},
{
@ -898,44 +979,47 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
]
},
{
id: MenuId.notifications_center,
id: MenuId.security_settings,
pages: [
{id: MenuId.notification_inbox},
{id: MenuId.notification_sent},
{id: MenuId.notification_recipients},
{id: MenuId.notification_templates},
{id: MenuId.notification_rules}
{
id: MenuId.oauth2,
pages: [
{id: MenuId.clients}
]
},
{id: MenuId.audit_log}
]
},
{
id: MenuId.mobile_center,
id: MenuId.platform_section,
pages: [
{id: MenuId.mobile_bundles},
{id: MenuId.mobile_apps}
{id: MenuId.version_control},
{
id: MenuId.settings,
pages: [
{id: MenuId.home_settings},
{id: MenuId.notification_settings},
{id: MenuId.repository_settings},
{id: MenuId.auto_commit_settings},
{id: MenuId.trendz_settings},
{id: MenuId.ai_models}
]
},
{id: MenuId.api_usage}
]
},
{id: MenuId.api_usage},
{
id: MenuId.settings,
id: MenuId.edge_management,
pages: [
{id: MenuId.home_settings},
{id: MenuId.notification_settings},
{id: MenuId.repository_settings},
{id: MenuId.auto_commit_settings},
{id: MenuId.trendz_settings},
{id: MenuId.ai_models}
{id: MenuId.edges},
{id: MenuId.rulechain_templates}
]
},
{
id: MenuId.security_settings,
id: MenuId.mobile_center,
pages: [
{id: MenuId.audit_log},
{
id: MenuId.oauth2,
pages: [
{id: MenuId.clients}
]
}
{id: MenuId.mobile_bundles},
{id: MenuId.mobile_apps}
]
}
]
@ -944,115 +1028,33 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
Authority.CUSTOMER_USER,
[
{id: MenuId.home},
{id: MenuId.alarms},
{id: MenuId.dashboards},
{
id: MenuId.entities,
id: MenuId.monitor,
pages: [
{id: MenuId.devices},
{id: MenuId.assets},
{id: MenuId.entity_views}
{id: MenuId.dashboards},
{
id: MenuId.alarms_center,
pages: [
{id: MenuId.alarms}
]
},
{
id: MenuId.notifications_center,
pages: [
{id: MenuId.notification_inbox}
]
}
]
},
{id: MenuId.edge_instances},
{
id: MenuId.notifications_center,
id: MenuId.entities,
pages: [
{id: MenuId.notification_inbox}
{id: MenuId.devices},
{id: MenuId.assets},
{id: MenuId.entity_views}
]
}
]
]
]);
const defaultHomeSectionMap = new Map<Authority, HomeSectionReference[]>([
[
Authority.SYS_ADMIN,
[
{
name: 'tenant.management',
places: [MenuId.tenants, MenuId.tenant_profiles]
},
{
name: 'widget.management',
places: [MenuId.widget_library]
},
{
name: 'admin.system-settings',
places: [MenuId.general, MenuId.mail_server,
MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.domains,
MenuId.clients, MenuId.two_fa, MenuId.resources_library, MenuId.queues]
}
]
],
[
Authority.TENANT_ADMIN,
[
{
name: 'rulechain.management',
places: [MenuId.rule_chains]
},
{
name: 'customer.management',
places: [MenuId.customers]
},
{
name: 'asset.management',
places: [MenuId.assets, MenuId.asset_profiles]
},
{
name: 'device.management',
places: [MenuId.devices, MenuId.device_profiles, MenuId.otaUpdates]
},
{
name: 'entity-view.management',
places: [MenuId.entity_views]
},
{
name: 'edge.management',
places: [MenuId.edges, MenuId.rulechain_templates]
},
{
name: 'dashboard.management',
places: [MenuId.widget_library, MenuId.dashboards]
},
{
name: 'version-control.management',
places: [MenuId.version_control]
},
{
name: 'audit-log.audit',
places: [MenuId.audit_log, MenuId.api_usage]
},
{
name: 'admin.system-settings',
places: [MenuId.home_settings, MenuId.resources_library, MenuId.repository_settings, MenuId.auto_commit_settings, MenuId.trendz_settings]
}
]
],
[
Authority.CUSTOMER_USER,
[
{
name: 'asset.view-assets',
places: [MenuId.assets]
},
{
name: 'device.view-devices',
places: [MenuId.devices]
},
{
name: 'entity-view.management',
places: [MenuId.entity_views]
},
{
name: 'edge.management',
places: [MenuId.edge_instances]
},
{
name: 'dashboard.view-dashboards',
places: [MenuId.dashboards]
}
{id: MenuId.edge_instances}
]
]
]);
@ -1062,10 +1064,9 @@ export const buildUserMenu = (authState: AuthState): Array<MenuSection> => {
return (references || []).map(ref => referenceToMenuSection(authState, ref)).filter(section => !!section);
};
export const buildUserHome = (authState: AuthState, availableMenuSections: MenuSection[]): Array<HomeSection> => {
const references = defaultHomeSectionMap.get(authState.authUser.authority);
return (references || []).map(ref =>
homeReferenceToHomeSection(availableMenuSections, ref)).filter(section => !!section);
export const buildUserHome = (currentMenuSections: MenuSection[]): Array<HomeSection> => {
return (currentMenuSections || []).map(section =>
menuSectionToHomeSection(section)).filter(section => !!section);
};
const referenceToMenuSection = (authState: AuthState, reference: MenuReference): MenuSection | undefined => {
@ -1103,14 +1104,18 @@ const filterMenuReference = (authState: AuthState, reference: MenuReference): bo
}
};
const homeReferenceToHomeSection = (availableMenuSections: MenuSection[], reference: HomeSectionReference): HomeSection | undefined => {
const places = reference.places.map(id => availableMenuSections.find(m => m.id === id)).filter(p => !!p);
if (places.length) {
return {
name: reference.name,
places
};
} else {
return undefined;
const menuSectionToHomeSection = (section: MenuSection): HomeSection => {
if (section.id !== MenuId.home) {
if (section.type === 'link') {
return {
name: section.name,
places: [ section ]
}
} else if (section.type === 'toggle' && section.pages?.length) {
return {
name: section.name,
places: section.pages
};
}
}
};
}

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

@ -22,7 +22,7 @@ import { filter, map, take } from 'rxjs/operators';
import { buildUserHome, buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models';
import { Observable, ReplaySubject, Subject } from 'rxjs';
import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router';
import { ActivationEnd, NavigationEnd, Router } from '@angular/router';
@Injectable({
providedIn: 'root'
@ -32,6 +32,7 @@ export class MenuService {
private currentMenuSections: Array<MenuSection>;
private menuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
private homeSections$: Subject<Array<HomeSection>> = new ReplaySubject<Array<HomeSection>>(1);
private _availableMenuSections: Array<MenuSection> = [];
private availableMenuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
private availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items))
@ -51,6 +52,9 @@ export class MenuService {
this.updateOpenedMenuSections();
}
);
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(() => {
this.updateActiveMenuSections();
});
}
private buildMenu() {
@ -58,11 +62,12 @@ export class MenuService {
(authState: AuthState) => {
if (authState.authUser) {
this.currentMenuSections = buildUserMenu(authState);
this._availableMenuSections = this.allMenuSections(this.currentMenuSections);
this.updateActiveMenuSections();
this.updateOpenedMenuSections();
this.menuSections$.next(this.currentMenuSections);
const availableMenuSections = this.allMenuSections(this.currentMenuSections);
this.availableMenuSections$.next(availableMenuSections);
const homeSections = buildUserHome(authState, availableMenuSections);
this.availableMenuSections$.next(this._availableMenuSections);
const homeSections = buildUserHome(this.currentMenuSections);
this.homeSections$.next(homeSections);
}
}
@ -70,11 +75,10 @@ export class MenuService {
}
private updateOpenedMenuSections() {
const url = this.router.url;
const openedMenuSections = getCurrentOpenedMenuSections(this.store);
if (this.currentMenuSections?.length) {
this.currentMenuSections.filter(section => section.type === 'toggle' &&
(url.startsWith(section.path) || openedMenuSections.includes(section.path))).forEach(
(openedMenuSections.includes(section.path) || section.active)).forEach(
section => section.opened = true
);
}
@ -112,6 +116,27 @@ export class MenuService {
return this.homeSections$;
}
private updateActiveMenuSections() {
const url = this.router.url;
const activeMenuSection = this._availableMenuSections.find(section => section.path === url);
this._availableMenuSections.forEach((section: MenuSection) => {
section.active = this.isSectionActive(section, activeMenuSection);
});
}
private isSectionActive(section: MenuSection, activeMenuSection: MenuSection): boolean {
if (activeMenuSection === section) {
return true;
} else if (section.pages?.length) {
for (const page of section.pages) {
if (this.isSectionActive(page, activeMenuSection)) {
return true;
}
}
}
return false;
}
public availableMenuLinks(): Observable<Array<MenuSection>> {
return this.availableMenuLinks$;
}

2
ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ dialogTitle | translate }}</h2>
<span class="flex-1"></span>
<div tb-help="aiModels"></div>

2
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html

@ -16,7 +16,7 @@
-->
<form style="width: 800px">
<mat-toolbar class="justify-between" color="primary">
<mat-toolbar class="tb-dialog-header justify-between">
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2>
<button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon>

2
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -16,7 +16,7 @@
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex h-full flex-col">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="alarmRules"></div>

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

@ -17,19 +17,19 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html

@ -16,7 +16,7 @@
-->
<form>
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar class="justify-between" color="primary">
<mat-toolbar class="tb-dialog-header justify-between">
<h2 translate>filter.complex-filter</h2>
<button mat-icon-button
(click)="cancel()"

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar class="justify-between" color="primary">
<mat-toolbar class="tb-dialog-header justify-between">
<h2>{{(data.isAdd ? 'alarm-rule.add-filter' : 'alarm-rule.edit-filter') | translate}}</h2>
<button mat-icon-button
(click)="cancel()"

2
ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html

@ -16,7 +16,7 @@
-->
<form style="width: 600px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<span class="flex-1"></span>
<button mat-icon-button (click)="close()" type="button">
<mat-icon class="material-icons">close</mat-icon>

2
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="alarmFormGroup" style="width: 600px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'alarm.alarm-details' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ title | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar class="min-w-0" color="primary">
<mat-toolbar class="tb-dialog-header min-w-0">
<h2>{{ 'api-key.generate-title' | translate }}</h2>
<span class="flex-1"></span>
<div tb-help="apiKeys"></div>

2
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar class="min-w-0" color="primary">
<mat-toolbar class="tb-dialog-header min-w-0">
<h2>{{ 'api-key.generated-api-key-title' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.html

@ -16,7 +16,7 @@
-->
<div class="api-keys-dialog-container flex flex-col xs:h-full">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'api-key.manage-api-keys' | translate }}</h2>
<span class="flex-1"></span>
<div tb-help="apiKeys"></div>

2
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 420px; max-width: 420px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isTelemetry ? 'attribute.add-telemetry' : 'attribute.add') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="addWidgetFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>attribute.add-widget-to-dashboard</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>audit-log.audit-log-details</h2>
<span class="flex-1"></span>
<button mat-icon-button

6
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html

@ -17,19 +17,19 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -16,7 +16,7 @@
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex flex-col">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="calculatedField"></div>

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html

@ -16,7 +16,7 @@
-->
<form #testScriptContainer class="cf-test-dialog-container size-full" [formGroup]="calculatedFieldScriptTestFormGroup">
<mat-toolbar class="flex justify-between" color="primary">
<mat-toolbar class="tb-dialog-header flex justify-between">
<h2>{{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})</h2>
<button mat-icon-button
(click)="cancel()"

4
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -16,13 +16,13 @@
-->
<form class="tb-add-widget-dialog" [formGroup]="widgetFormGroup">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<div class="tb-add-widget-toolbar">
<h2>{{'widget.add' | translate}}:</h2>
<span class="flex-1">{{data.widgetInfo.widgetName}}</span>
<div class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;">
@if (widgetConfig.hasBasicMode) {
<tb-toggle-select appearance="fill-invert" selectMediaBreakpoint="xs"
<tb-toggle-select appearance="fill" selectMediaBreakpoint="xs"
[(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>dashboard.update-image</h2>
<span class="flex-1"></span>
<button mat-icon-button

92
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -15,6 +15,20 @@
limitations under the License.
-->
<ng-template #dashboardBreadcrumbsTpl>
<tb-states-component class="lt-md:flex-1"
[statesControllerId]="dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
[dashboardId]="setStateDashboardId ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[currentState]="currentState"
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
</ng-template>
<div class="tb-dashboard-page mat-content flex-1" [class.mobile-app]="isMobileApp && !isEdit" tb-toast
tb-fullscreen [fullscreenElement]="elRef.nativeElement" [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
@ -26,7 +40,7 @@
<div class="tb-dashboard-action-panels flex flex-col items-stretch justify-center gt-sm:flex-row gt-sm:items-center gt-sm:justify-between">
<div class="tb-dashboard-action-panel tb-left-panel" [class.!hidden]="isMobileApp && !isEdit">
@if (!isPublicUser() && forceFullscreen) {
<tb-user-menu class="gt-sm:!hidden" displayUserInfo="true">
<tb-user-menu class="gt-sm:!hidden">
</tb-user-menu>
}
@if (isEdit) {
@ -50,17 +64,24 @@
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div>
<tb-states-component class="lt-md:flex-1"
[statesControllerId]="'default'"
[dashboardCtrl]="this"
[dashboardId]="setStateDashboardId ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[currentState]="currentState"
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
} @else {
@if (!isPublicUser()) {
<tb-notification-bell class="gt-sm:!hidden"></tb-notification-bell>
}
<tb-breadcrumb class="lt-md:flex-1"
[componentBreadcrumbsTpl]="dashboardBreadcrumbsTpl">
</tb-breadcrumb>
}
<tb-states-component class="lt-md:flex-1"
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
[dashboardId]="setStateDashboardId ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[currentState]="currentState"
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
@if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}"
@ -79,6 +100,13 @@
@if (showDashboardLogo()) {
<tb-logo [link]="dashboardLogoLink" [src]="dashboardLogo | image | async" class="dashboard_logo"></tb-logo>
}
@if (!isEdit) {
<button mat-icon-button
[class.!hidden]="forceFullscreen || isFullscreen"
class="-ml-2 gt-sm:!hidden" (click)="toggleSidenav()">
<mat-icon class="material-icons">menu</mat-icon>
</button>
}
</div>
<div class="tb-dashboard-action-panel">
<button [class.!hidden]="!showCloseToolbar()" mat-icon-button class="close-action"
@ -88,7 +116,7 @@
<mat-icon>arrow_forward</mat-icon>
</button>
@if (!isPublicUser() && forceFullscreen) {
<tb-user-menu class="lt-md:!hidden" displayUserInfo="true">
<tb-user-menu class="lt-md:!hidden">
</tb-user-menu>
}
<button [class.!hidden]="!showRightLayoutSwitch()" mat-icon-button
@ -97,12 +125,20 @@
(click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button>
<button [class.!hidden]="hideFullscreenButton()" class="xs:!hidden" mat-icon-button
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<div class="flex flex-row-reverse items-center lt-md:!hidden">
@if (!isEdit && !isPublicUser()) {
<tb-notification-bell></tb-notification-bell>
}
<button [class.!hidden]="hideFullscreenButton()" mat-icon-button
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<mat-divider
[class.!hidden]="readonly || isEdit"
class="tb-toolbar-divider -mr-1.5 ml-2 lt-md:!hidden lt-lg:mr-[-3px]" vertical></mat-divider>
<button [class.!hidden]="!currentDashboardId || isMobileApp || !displayExport() || isEdit" mat-icon-button
matTooltip="{{'dashboard.export' | translate}}"
matTooltipPosition="below"
@ -191,7 +227,7 @@
{{ 'dashboard.settings' | translate }}
</button>
}
<button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !isTenantAdmin() || !displayUpdateDashboardImage()" mat-icon-button
<button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !canEdit() || readonly || !displayUpdateDashboardImage()" mat-icon-button
matTooltip="{{'dashboard.update-image' | translate}}"
matTooltipPosition="below"
(click)="updateDashboardImage($event)">
@ -264,15 +300,25 @@
[aliasController]="dashboardCtx.aliasController">
</tb-aliases-entity-select>
@if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) {
<tb-dashboard-select [(ngModel)]="currentDashboardId"
<tb-dashboard-select class="ml-2" [(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId"
[dashboardsScope]="currentDashboardScope">
</tb-dashboard-select>
}
@if (!isEdit && !widgetEditMode) {
<div class="-mr-1.5 md-lg:!hidden lt-md:!hidden" [class.!hidden]="githubBadge.hideGithubBadge">
<tb-github-badge #githubBadge ></tb-github-badge>
</div>
}
</div>
</div>
</tb-dashboard-toolbar>
@if (hideMainToolbar && toolbarOpened && (homeService.hideLoadingBar$ | async) === false && (isLoading$ | async)) {
<mat-progress-bar color="warn" style="z-index: 10; margin-bottom: -4px; width: 100%;" mode="indeterminate"
>
</mat-progress-bar>
}
</section>
<section class="tb-dashboard-container tb-absolute-fill"
tb-toast toastTarget="dashboardRoot"
@ -350,7 +396,7 @@
@if (!readonly && (hideToolbar || widgetEditMode)) {
<section data-html2canvas-ignore class="tb-header-buttons tb-enter-edit-mode flex flex-row flex-wrap items-end justify-start">
<button [class.!hidden]="isEdit"
color="primary"
color="#fff"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
@ -359,7 +405,7 @@
<mat-icon>edit</mat-icon>
</button>
<button [class.!hidden]="!isEdit"
color="primary"
color="#fff"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="saveDashboard()"
@ -369,7 +415,7 @@
<mat-icon>done</mat-icon>
</button>
<button [class.!hidden]="!isEdit"
color="primary"
color="#fff"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"

11
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -15,10 +15,10 @@
*/
@import "../../../../../scss/constants";
$toolbar-height: 50px !default;
$fullscreen-toolbar-height: 64px !default;
$mobile-toolbar-height: 84px !default;
$mobile-app-toolbar-height: 40px !default;
$toolbar-height: 56px !default;
$fullscreen-toolbar-height: 56px !default;
$mobile-toolbar-height: 96px !default;
$mobile-app-toolbar-height: 48px !default;
tb-dashboard-page {
display: flex;
@ -79,8 +79,7 @@ div.tb-dashboard-page {
}
.mat-divider-vertical.tb-toolbar-divider {
border-right-color: rgba(255,255,255,0.75);
height: 30px;
height: 100%;
}
.tb-dashboard-container {

20
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -149,6 +149,7 @@ import {
MoveWidgetsDialogResult
} from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import { HttpStatusCode } from '@angular/common/http';
import { HomeService } from '@core/services/home.service';
// @dynamic
@Component({
@ -199,6 +200,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return ((this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit) || (this.isEditingWidget || this.isAddingWidget);
}
@Input()
hideMainToolbar = true;
@Input()
syncStateWithQueryParam = true;
@ -267,7 +271,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
dashboardLogoLink = this.getDashboardLogoLink();
private dashboardLogoCache: SafeUrl;
private defaultDashboardLogo = 'assets/logo_title_white.svg';
private defaultDashboardLogo = 'assets/logo_title_black.svg';
private dashboardResize$: ResizeObserver;
@ -374,7 +378,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
public elRef: ElementRef,
private injector: Injector) {
private injector: Injector,
public homeService: HomeService) {
super(store);
if (isDefinedAndNotNull(this.embeddedValue)) {
this.embedded = this.embeddedValue;
@ -382,6 +387,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
ngOnInit() {
if (this.hideMainToolbar) {
this.homeService.setHideMainToolbar(true);
}
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
let dashboardPageInitData: DashboardPageInitData;
@ -867,6 +875,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return this.authUser.authority === Authority.SYS_ADMIN;
}
public canEdit(): boolean {
return this.isTenantAdmin() || (this.isSystemAdmin() && this.widgetEditMode);
}
public exportDashboard($event: Event) {
if ($event) {
$event.preventDefault();
@ -1745,6 +1757,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
toggleSidenav() {
this.homeService.toggleSideBar.emit();
}
get showMainLayoutFiller(): boolean {
const layoutMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint)?.maxWidth || Infinity;
const dashboardMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.dashboardCtx.breakpoint)?.maxWidth || Infinity;

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 750px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>{{settings ? 'dashboard.settings' : 'layout.settings'}}</h2>
<span class="flex-1"></span>
<button mat-icon-button

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html

@ -20,6 +20,7 @@
[embedded]="true"
[syncStateWithQueryParam]="false"
[hideToolbar]="true"
[hideMainToolbar]="false"
[currentState]="currentState"
[dashboard]="dashboard"
[parentDashboard]="parentDashboard"

9
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html

@ -15,18 +15,17 @@
limitations under the License.
-->
<mat-fab-toolbar [isOpen]="toolbarOpened" color="primary" direction="left"
[class.is-fullscreen]="forceFullscreen"
[class.mat-elevation-z1]="forceFullscreen && toolbarOpened">
<mat-fab-toolbar [isOpen]="toolbarOpened" direction="left"
[class.is-fullscreen]="forceFullscreen">
<mat-fab-trigger>
<button mat-fab color="primary"
<button mat-fab color="#fff"
[matTooltip]="!toolbarOpened ? ('dashboard.open-toolbar' | translate) : ''"
matTooltipPosition="below"
(click)="onTriggerClick()">
<mat-icon>more_horiz</mat-icon>
</button>
</mat-fab-trigger>
<mat-toolbar color="primary">
<mat-toolbar>
<mat-fab-actions class="mat-toolbar-tools">
<ng-content></ng-content>
</mat-fab-actions>

32
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -15,13 +15,13 @@
*/
@import "../../../../../scss/constants";
$toolbar-height: 50px !default;
$fullscreen-toolbar-height: 64px !default;
$mobile-toolbar-height: 80px !default;
$half-mobile-toolbar-height: 40px !default;
$mobile-toolbar-height-total: 84px !default;
$toolbar-height: 56px !default;
$fullscreen-toolbar-height: 56px !default;
$mobile-toolbar-height: 96px !default;
$half-mobile-toolbar-height: 48px !default;
$mobile-toolbar-height-total: 96px !default;
$mobile-app-toolbar-height: 40px !default;
$mobile-app-toolbar-height: 48px !default;
div.tb-dashboard-page.mobile-app {
tb-dashboard-toolbar {
@ -95,6 +95,12 @@ tb-dashboard-toolbar {
}
}
&.mat-is-open {
.mat-fab-toolbar-wrapper {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}
.mat-fab-toolbar-wrapper {
height: $mobile-toolbar-height-total;
@ -121,12 +127,12 @@ tb-dashboard-toolbar {
mat-fab-actions {
margin-top: 0;
padding: 0 16px !important;
font-size: 16px;
@media #{$mat-lt-md} {
height: $mobile-toolbar-height;
max-height: $mobile-toolbar-height;
padding: 0 8px !important;
}
.close-action {
@ -138,14 +144,14 @@ tb-dashboard-toolbar {
height: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
height: $toolbar-height;
}
.tb-dashboard-action-panels {
height: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
height: $toolbar-height;
}
.tb-dashboard-action-panel {
@ -161,14 +167,14 @@ tb-dashboard-toolbar {
}
@media #{$mat-lt-md} {
padding-left: 12px;
// padding-left: 12px;
}
@media #{$mat-xs} {
gap: 3px;
padding-left: 0;
&.tb-left-panel {
padding-left: 12px;
// padding-left: 12px;
}
}
@ -198,14 +204,14 @@ tb-dashboard-toolbar {
&.tb-left-panel {
place-content: center flex-end;
}
height: 46px;
height: $toolbar-height;
}
> div {
height: $half-mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
height: $toolbar-height;
}
}

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -16,11 +16,11 @@
-->
<div class="tb-dashboard-widget-select">
<mat-toolbar color="primary" class="tb-widget-select-header flex flex-col">
<mat-toolbar class="tb-widget-select-header flex flex-col">
<div class="tb-widget-select-title-row flex flex-row items-center">
@if (showTopLevelWidgetModeToggle()) {
<tb-toggle-select class="tb-top-level-widget-mode-toggle"
appearance="fill-invert"
appearance="fill"
[disablePagination]="true"
[(ngModel)]="selectWidgetMode">
<tb-toggle-option value="installed" [template]="installedWidgetsOption"></tb-toggle-option>

29
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -35,15 +35,12 @@ tb-dashboard-widget-select {
width: 100%;
height: auto;
gap: 16px;
color: #fff;
padding: 12px 12px 12px 24px;
button.mat-mdc-icon-button.mat-mdc-button-base {
--mdc-icon-button-state-layer-size: 40px;
--mat-icon-button-state-layer-size: 40px;
--mdc-icon-button-icon-color: #fff;
padding: 8px;
color: #fff;
}
}
@ -56,7 +53,6 @@ tb-dashboard-widget-select {
font-size: 24px;
line-height: 32px;
font-weight: 400;
color: #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@ -90,11 +86,6 @@ tb-dashboard-widget-select {
}
}
}
&.tb-fill.tb-invert {
.mat-button-toggle.mat-button-toggle-appearance-standard {
color: #FFF;
}
}
}
}
}
@ -106,14 +97,14 @@ tb-dashboard-widget-select {
.tb-widget-select-search {
height: 40px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 8px;
caret-color: $tb-primary-color;
color: rgba(255, 255, 255, 0.84);
color: rgba(0, 0, 0, 0.54);
mat-icon {
color: rgba(255, 255, 255, 0.8);
color: rgba(0, 0, 0, 0.54);
}
input {
@ -124,23 +115,22 @@ tb-dashboard-widget-select {
font-size: 16px;
line-height: 24px;
background: transparent;
color: #fff;
border: none;
outline: none;
&::placeholder {
color: rgba(255, 255, 255, 0.8);
color: rgba(0, 0, 0, 0.54);
opacity: 1;
}
}
button.mat-mdc-icon-button mat-icon {
color: rgba(255, 255, 255, 0.8);
color: rgba(0, 0, 0, 0.54);
}
&.focus {
background: #fff;
border-color: rgba(255, 255, 255, 0.15);
border-color: rgba(0, 0, 0, 0.15);
caret-color: $tb-primary-color;
mat-icon {
@ -165,14 +155,9 @@ tb-dashboard-widget-select {
}
.tb-widget-select-sort {
color: #fff;
font-weight: 500;
font-size: 14px;
letter-spacing: 0.25px;
mat-icon {
color: #fff;
}
}
}

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>layout.add-new-breakpoint</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="layoutsFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>layout.manage</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="move()">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>dashboard.move-all-widgets</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="stateFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-state' }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

19
ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html

@ -15,11 +15,16 @@
limitations under the License.
-->
<mat-select class="default-state-controller" [class.!hidden]="!displayStateSelection()"
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
@for (stateKv of states | keyvalue; track stateKv) {
<mat-option [value]="stateKv.key">
{{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
<div class="flex flex-row items-center justify-start gap-2" [class.tb-mobile]="isMobile">
@if ((breadcrumbs$ | async).length > 1) {
<span class='state-divider'> > </span>
}
</mat-select>
<mat-select class="default-state-controller" [class.!hidden]="!displayStateSelection()"
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
@for (stateKv of states | keyvalue; track stateKv) {
<mat-option [value]="stateKv.key">
{{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
}
</mat-select>
</div>

44
ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.scss

@ -16,13 +16,55 @@
:host {
pointer-events: all;
width: min-content; //for Safari
.state-divider {
display: none;
padding: 0;
width: 20px;
min-width: 20px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
:host-context(.tb-breadcrumb) {
.state-divider {
display: block;
}
.tb-mobile {
.state-divider {
display: none;
}
}
}
}
:host-context(.tb-breadcrumb) {
::ng-deep {
.mat-mdc-select.default-state-controller {
--mat-select-trigger-text-size: 16px;
--mat-select-enabled-trigger-text-color: rgba(0, 0, 0, 0.76);
}
}
.tb-mobile {
::ng-deep {
.mat-mdc-select.default-state-controller {
--mat-select-trigger-text-size: 24px;
--mat-select-trigger-text-line-height: 32px;
--mat-select-trigger-text-weight: 400;
}
}
}
}
:host ::ng-deep {
.mat-mdc-select.default-state-controller {
--mat-select-trigger-text-size: 14px;
.mat-mdc-select-value {
max-width: 200px;
font-size: 14px;
}
.mat-mdc-select-arrow {
width: 24px;

6
ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts

@ -27,6 +27,7 @@ import { base64toObj, objToBase64 } from '@app/core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { MobileService } from '@core/services/mobile.service';
import { BreadcrumbService } from '@core/services/breadcrumb.service';
@Component({
selector: 'tb-default-state-controller',
@ -36,6 +37,8 @@ import { MobileService } from '@core/services/mobile.service';
})
export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
breadcrumbs$ = this.breadcrumbService.breadcrumbs$;
constructor(protected router: Router,
protected route: ActivatedRoute,
protected ngZone: NgZone,
@ -43,7 +46,8 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
private utils: UtilsService,
private entityService: EntityService,
private mobileService: MobileService,
private dashboardUtils: DashboardUtilsService) {
private dashboardUtils: DashboardUtilsService,
private breadcrumbService: BreadcrumbService) {
super(router, route, ngZone, statesControllerService);
}

42
ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html

@ -16,29 +16,33 @@
-->
<div class="entity-state-controller">
@if (!isMobile || stateObject.length === 1) {
<div class="flex flex-row items-center justify-start">
@for (state of stateObject; track state; let i = $index; let isLast = $last) {
@if (!isMobile) {
<div class="flex flex-row items-center justify-start gap-2">
@for (state of stateObject; track state.id; let i = $index; let isLast = $last;) {
@if ((breadcrumbs$ | async).length > 1 || i > 0) {
<span class='state-divider' [class.last-divider]="isLast"> > </span>
}
<span class="state-entry"
[style.font-weight]="isLast ? 'bold' : 'normal'"
[style.cursor]="isLast ? 'default' : 'pointer'" (click)="navigatePrevState(i)">
[class.last-entry]="isLast"
(click)="navigatePrevState(i)">
{{getStateName(i)}}
<span class='state-divider' [class.!hidden]="isLast"> > </span>
</span>
}
</div>
}
@if (isMobile && stateObject.length > 1) {
<mat-select
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
@for (state of stateObject; track state; let i = $index) {
<mat-option [value]="i">
{{getStateName(i)}}
</mat-option>
}
</mat-select>
} @else {
@if (stateObject.length > 1) {
<mat-select
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
@for (state of stateObject; track state.id; let i = $index) {
<mat-option [value]="i">
{{getStateName(i)}}
</mat-option>
}
</mat-select>
} @else {
<h1 (click)="navigatePrevState(0)">
{{getStateName(0)}}
</h1>
}
}
</div>

46
ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.scss

@ -14,37 +14,65 @@
* limitations under the License.
*/
:host {
display: block;
overflow: hidden;
.entity-state-controller {
color: rgba(0,0,0,0.76);
.state-divider {
padding: 0 15px;
padding: 0;
width: 20px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
opacity: .75;
&.last-divider {
opacity: 1;
}
}
.state-entry {
pointer-events: all;
overflow: hidden;
font-size: 18px;
font-size: 16px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
color: inherit;
outline: none;
cursor: pointer;
&:not(.last-entry) {
opacity: .75;
transition: opacity .35s;
&:hover,
&:focus {
opacity: 1;
}
}
&.last-entry {
cursor: default;
}
}
h1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24px;
line-height: 32px;
font-weight: 400;
}
}
}
:host ::ng-deep {
.mat-mdc-select {
.mat-mdc-select-value {
max-width: 200px;
.mat-mdc-select-value-text {
font-size: 18px;
font-weight: 700;
}
}
--mat-select-trigger-text-size: 24px;
--mat-select-trigger-text-line-height: 32px;
--mat-select-trigger-text-weight: 400;
--mat-select-enabled-trigger-text-color: rgba(0,0,0,0.76);
.mat-mdc-select-arrow {
width: 24px;
}

6
ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts

@ -29,6 +29,7 @@ import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
import { map, tap } from 'rxjs/operators';
import { MobileService } from '@core/services/mobile.service';
import { BreadcrumbService } from '@core/services/breadcrumb.service';
@Component({
selector: 'tb-entity-state-controller',
@ -38,6 +39,8 @@ import { MobileService } from '@core/services/mobile.service';
})
export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
breadcrumbs$ = this.breadcrumbService.breadcrumbs$;
selectedStateIndex = -1;
constructor(protected router: Router,
@ -47,7 +50,8 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
private utils: UtilsService,
private entityService: EntityService,
private mobileService: MobileService,
private dashboardUtils: DashboardUtilsService) {
private dashboardUtils: DashboardUtilsService,
private breadcrumbService: BreadcrumbService) {
super(router, route, ngZone, statesControllerService);
}

2
ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>dashboard.manage-states</h2>
<span class="flex-1"></span>
<button mat-icon-button

1
ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts

@ -35,4 +35,5 @@ export class DashboardViewComponent extends PageComponent {
private route: ActivatedRoute) {
super(store);
}
}

2
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -78,7 +78,7 @@
</mat-menu>
<div [class]="dashboardClass" id="gridster-background">
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
@for (widget of dashboardWidgets; track widget) {
@for (widget of dashboardWidgets; track widget.widgetId) {
<gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}">
<tb-widget-container
[gridsterItem]="gridsterItem"

2
ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html

@ -16,7 +16,7 @@
-->
<form>
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>layout.select</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="stateFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>dashboard.select-state</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -16,7 +16,7 @@
-->
<header>
<mat-toolbar class="details-toolbar" color="primary" [style.height]="headerHeightPx+'px'">
<mat-toolbar class="details-toolbar" [style.height]="headerHeightPx+'px'">
<div class="mat-toolbar-tools flex flex-row items-center justify-start" style="height: 100%;">
<div class="mat-toolbar-tools tb-details-title-header flex flex-1 flex-col items-start justify-start" [class.!hidden]="showSearchPane">
<div class="tb-details-title flex flex-row items-center justify-start">

1
ui-ngx/src/app/modules/home/components/details-panel.component.scss

@ -22,6 +22,7 @@
flex-direction: column;
.mat-toolbar.details-toolbar {
padding: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}

2
ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<button mat-raised-button
<button mat-stroked-button
ngxClipboard
[cbContent]="credential"
(cbOnSuccess)="onCopyCredential()"

2
ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts

@ -30,7 +30,7 @@ import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-copy-device-credentials',
templateUrl: './copy-device-credentials.component.html',
styleUrls: [],
styles: ':host { display: contents; }',
standalone: false
})
export class CopyDeviceCredentialsComponent implements OnInit, OnDestroy {

2
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="add()" style="min-width: 400px; width: 750px;" [style]="entitiesTableConfig.addDialogStyle">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>{{ translations.add }}</h2>
<span class="flex-1"></span>
<div [tb-help]="helpLinkId()"></div>

2
ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar class="flex flex-row" color="primary">
<mat-toolbar class="tb-dialog-header flex flex-row">
<h2 translate>{{ title }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>filter.complex-filter</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary" class="justify-between">
<mat-toolbar class="tb-dialog-header justify-between">
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2>
<button mat-icon-button
(click)="cancel()"

2
ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="filterUserInfoFormGroup" (ngSubmit)="save()" style="width: 500px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{(data.readonly ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="filtersFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ title | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
<mat-toolbar color="primary" class="justify-between">
<mat-toolbar class="tb-dialog-header justify-between">
<h2>{{ filter.filter | customTranslate }}</h2>
<button mat-icon-button
(click)="cancel()"

2
ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.scss

@ -14,6 +14,7 @@
* limitations under the License.
*/
:host {
pointer-events: auto;
&:has(section) {
height: 100%;
}
@ -35,6 +36,7 @@
}
.mdc-icon-button {
--mat-icon-color: #fff;
background-color: #162C41;
&:hover {

31
ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, OnDestroy } from '@angular/core';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { GitHubService } from '@core/http/git-hub.service';
import { Store } from '@ngrx/store';
import { selectAuthUser, selectIsAuthenticated } from '@core/auth/auth.selectors';
@ -32,18 +32,27 @@ const SETTINGS_KEY = 'HIDE_GITHUB_STAR_BUTTON';
styleUrl: './github-badge.component.scss',
standalone: false
})
export class GithubBadgeComponent implements OnDestroy {
export class GithubBadgeComponent implements OnInit, OnDestroy {
get hideGithubBadge(): boolean {
return this.hide;
}
githubStar = 0;
private hide = false;
private stopWatch$ = new Subject<void>();
constructor(private gitHubService: GitHubService,
private localStorageService: LocalStorageService,
private store: Store<AppState>,) {
const hide = this.localStorageService.getItem(SETTINGS_KEY) ?? false;
private store: Store<AppState>,
private cd: ChangeDetectorRef) {
}
ngOnInit() {
this.hide = this.localStorageService.getItem(SETTINGS_KEY) ?? false;
if (!hide) {
if (!this.hide) {
this.store.select(selectIsAuthenticated).pipe(
filter((data) => data),
switchMap(() => this.store.select(selectAuthUser).pipe(take(1))),
@ -60,21 +69,25 @@ export class GithubBadgeComponent implements OnDestroy {
} else {
this.githubStar = 0
}
this.cd.detectChanges();
});
}
}
ngOnDestroy() {
this.stopWatch$.next();
this.stopWatch$.complete();
}
hideGithubStar($event: Event) {
$event?.stopPropagation();
this.localStorageService.setItem(SETTINGS_KEY, true);
this.hide = true;
this.githubStar = 0;
this.stopWatch$.next();
this.stopWatch$.complete();
this.cd.detectChanges();
}
ngOnDestroy() {
this.stopWatch$.next();
this.stopWatch$.complete();
}
}

35
ui-ngx/src/app/modules/home/components/github-badge/github-badge.module.ts

@ -0,0 +1,35 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { GithubBadgeComponent } from '@home/components/github-badge/github-badge.component';
@NgModule({
declarations:
[
GithubBadgeComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
GithubBadgeComponent
]
})
export class GithubBadgeModule { }

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

@ -202,6 +202,8 @@ import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key
import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component';
import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component";
import { EventsDialogComponent } from '@home/dialogs/events-dialog.component';
import { NotificationBellModule } from '@home/components/notification/notification-bell.module';
import { GithubBadgeModule } from '@home/components/github-badge/github-badge.module';
@NgModule({
declarations:
@ -374,7 +376,9 @@ import { EventsDialogComponent } from '@home/dialogs/events-dialog.component';
StatesControllerModule,
DeviceCredentialsModule,
DeviceProfileCommonModule,
EntityDebugSettingsButtonComponent
EntityDebugSettingsButtonComponent,
NotificationBellModule,
GithubBadgeModule
],
exports: [
RouterTabsComponent,

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html

@ -16,7 +16,7 @@
-->
<form>
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ getTitle() }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/notification/notification-bell.component.html

@ -16,5 +16,5 @@
-->
<button mat-icon-button #showNotificationButton (click)="showNotification($event, showNotificationButton)">
<mat-icon class="material-icons" aria-hidden="false" [matBadge]="(count$ | async)" [matBadgeHidden]="!(count$ | async)" matBadgeColor="warn" matBadgeSize="small">notifications</mat-icon>
<tb-icon aria-hidden="false" [matBadge]="(count$ | async)" [matBadgeHidden]="!(count$ | async)" matBadgeColor="warn" matBadgeSize="small">notifications_none</tb-icon>
</button>

38
ui-ngx/src/app/modules/home/components/notification/notification-bell.module.ts

@ -0,0 +1,38 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { NotificationBellComponent } from '@home/components/notification/notification-bell.component';
import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component';
@NgModule({
declarations:
[
NotificationBellComponent,
ShowNotificationPopoverComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
NotificationBellComponent,
ShowNotificationPopoverComponent
]
})
export class NotificationBellModule { }

2
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2 translate>device-profile.add</h2>
<span class="flex-1"></span>
<div [tb-help]="'deviceProfiles'"></div>

2
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 900px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()" style="width: 900px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'device-profile.edit-alarm-rule-additional-info' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 600px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isAdd ? 'asset-profile.add' : 'asset-profile.edit' ) | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

10
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html

@ -17,32 +17,32 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{'asset-profile.export' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[class.!hidden]="isEdit || entity?.default">
{{'asset-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{'asset-profile.delete' | translate }}
</button>
<div class="flex flex-row xs:flex-col">
<button mat-raised-button
<button mat-stroked-button
ngxClipboard
(cbOnSuccess)="onAssetProfileIdCopied($event)"
[cbContent]="entity?.id?.id"

2
ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 1000px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

10
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -17,32 +17,32 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{'device-profile.export' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[class.!hidden]="isEdit || entity?.default">
{{'device-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{'device-profile.delete' | translate }}
</button>
<div class="flex flex-row xs:flex-col">
<button mat-raised-button
<button mat-stroked-button
ngxClipboard
(cbOnSuccess)="onDeviceProfileIdCopied($event)"
[cbContent]="entity?.id?.id"

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="attributeFormGroup" (ngSubmit)="save()" style="min-width: 500px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>
{{ (readonly ? 'device-profile.lwm2m.view-attributes' : 'device-profile.lwm2m.edit-attributes') | translate : {name: name} }}
</h2>

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="addConfigServerFormGroup" style="width: 400px;">
<mat-toolbar class="flex flex-row" color="primary">
<mat-toolbar class="tb-dialog-header flex flex-row">
<h2 translate>device-profile.lwm2m.add-new-server-title</h2>
<span class="flex-1"></span>
<button mat-icon-button (click)="cancel()" type="button">

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="instancesFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar class="flex flex-row" color="primary">
<mat-toolbar class="tb-dialog-header flex flex-row">
<span style="font-weight: 500">{{data.objectName}}</span>&nbsp;#{{data.objectId}}
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 650px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

8
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html

@ -17,26 +17,26 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[class.!hidden]="isEdit || entity?.default">
{{'tenant-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{'tenant-profile.delete' | translate }}
</button>
<div class="flex flex-row xs:flex-col">
<button mat-raised-button
<button mat-stroked-button
ngxClipboard
(cbOnSuccess)="onTenantProfileIdCopied($event)"
[cbContent]="entity?.id?.id"

2
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="editDetailsFormGroup">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ title | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="relationFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar class="flex flex-row" color="primary">
<mat-toolbar class="tb-dialog-header flex flex-row">
<h2>{{ (isAdd ? 'relation.add' : 'relation.edit' ) | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html

@ -16,7 +16,7 @@
-->
<form (ngSubmit)="save()" style="width: 600px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'resource.add' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

8
ui-ngx/src/app/modules/home/components/resources/resources-library.component.html

@ -17,26 +17,26 @@
-->
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'downloadResource')"
[class.!hidden]="isEdit">
{{ 'resource.download' | translate }}
</button>
<button mat-raised-button color="primary"
<button mat-stroked-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'deleteResource')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'resource.delete' | translate }}
</button>
<div class="flex flex-row xs:flex-col">
<button mat-raised-button
<button mat-stroked-button
ngxClipboard
(cbOnSuccess)="onResourceIdCopied()"
[cbContent]="entity?.id?.id"

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="flex flex-1 flex-col" style="width: 100%;">
<div class="flex flex-1 flex-col" [class.mt-2]="(homeService.hideMainToolbar$ | async) === false" style="width: 100%;">
<div class="flex flex-row" style="width: 100%;">
@if (!routerOutlet?.activatedRouteData?.hideTabs && !hideCurrentTabs && (tabs$ | async).length > 1) {
<nav mat-tab-nav-bar mat-stretch-tabs="false" class="tb-router-tabs flex-1" [tabPanel]="tabPanel">

5
ui-ngx/src/app/modules/home/components/router-tabs.component.scss

@ -19,11 +19,14 @@
height: 100%;
.mat-mdc-tab-nav-bar.tb-router-tabs {
--mat-tab-container-height: 38px;
--mat-tab-divider-height: 1px;
--mat-tab-divider-color: rgba(0, 0, 0, 0.12);
a.mat-mdc-tab-link {
display: inline-flex;
align-items: center;
overflow: hidden;
line-height: 40px;
line-height: 38px;
min-width: 200px;
border-bottom: none;
.mat-icon {

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

@ -26,6 +26,7 @@ import { MenuSection } from '@core/services/menu.models';
import { ActiveComponentService } from '@core/services/active-component.service';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { HomeService } from '@core/services/home.service';
@Component({
selector: 'tb-router-tabs',
@ -46,10 +47,11 @@ export class RouterTabsComponent extends PageComponent implements OnInit {
tabs$: Observable<Array<MenuSection>>;
constructor(protected store: Store<AppState>,
private activatedRoute: ActivatedRoute,
public activatedRoute: ActivatedRoute,
public router: Router,
private menuService: MenuService,
private activeComponentService: ActiveComponentService) {
private activeComponentService: ActiveComponentService,
public homeService: HomeService) {
super(store);
}

2
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="actionsSettings" style="width: 900px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="widgetActionFormGroup" (ngSubmit)="save()" style="width: 900px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

4
ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html

@ -16,7 +16,7 @@
-->
<div class="dashboard-state-dialog flex flex-col" [style]="dialogStyle">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{title}}</h2>
<span class="flex-1"></span>
<button mat-icon-button (click)="close()" type="button">
@ -30,7 +30,7 @@
<div style="height: 4px;"></div>
}
<div class="dashboard-state-dialog-content flex flex-1 flex-col" mat-dialog-content style="padding: 8px;">
<tb-dashboard-page [embedded]="true" [syncStateWithQueryParam]="false" [hideToolbar]="hideToolbar"
<tb-dashboard-page [embedded]="true" [syncStateWithQueryParam]="false" [hideToolbar]="hideToolbar" [hideMainToolbar]="false"
[currentState]="state" [dashboard]="dashboard" [parentDashboard]="parentDashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
</div>
<div mat-dialog-actions class="flex items-center justify-end">

2
ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html

@ -16,7 +16,7 @@
-->
<form #addEntityForm="ngForm" [formGroup]="selectEntityFormGroup" (ngSubmit)="save()">
<mat-toolbar class="flex flex-row" color="primary">
<mat-toolbar class="tb-dialog-header flex flex-row">
<h2 translate>widgets.maps.select-entity</h2>
<span class="flex-1"></span>
<button mat-icon-button [matDialogClose]="null" type="button">

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
<a mat-stroked-button class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
<tb-icon matButtonIcon class="tb-mat-96">{{settings.icon}}</tb-icon>
<span>{{translatedName}}</span>
</a>

16
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss

@ -19,18 +19,14 @@
}
:host ::ng-deep {
.tb-nav-button {
.tb-nav-button.mat-mdc-outlined-button {
background: #fff;
width: 100%;
height: 100% !important;
&:hover {
border-bottom: none;
}
&:focus {
border-bottom: none;
}
display: flex;
flex-direction: column;
align-items: center;
color: rgba(0,0,0,0.76);
.mat-icon {
margin: auto !important;
}
@ -46,11 +42,5 @@
white-space: normal;
text-align: center;
}
&.mat-mdc-raised-button.mat-primary {
.mat-ripple-element {
opacity: 0.3;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html

@ -27,7 +27,7 @@
<mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
@for (place of sectionPlaces(section); track place) {
<mat-grid-tile>
<a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<a mat-stroked-button class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span>{{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}}</span>
</a>

15
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss

@ -42,19 +42,14 @@
padding: 16px;
}
}
.tb-card-button {
.tb-card-button.mat-mdc-outlined-button {
width: 100%;
height: 100%;
max-width: 240px;
&:hover {
border-bottom: none;
}
&:focus {
border-bottom: none;
}
display: flex;
flex-direction: column;
align-items: center;
color: rgba(0,0,0,0.76);
.mat-icon {
margin: auto;
}
@ -70,12 +65,6 @@
white-space: normal;
text-align: center;
}
&.mat-mdc-raised-button.mat-primary {
.mat-ripple-element {
opacity: 0.3;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="persistentFormGroup" (ngSubmit)="save()" style="min-width: 480px; max-width: 600px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ 'widgets.persistent-table.add-title' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="persistentFormGroup" style="min-width: 480px;">
<mat-toolbar color="primary">
<mat-toolbar class="tb-dialog-header">
<h2>{{ persistentFormGroup.get('rpcId').value }}</h2>
<span class="flex-1"></span>
<button mat-icon-button

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save