Browse Source

Add per-type item pages with separate routes and hero sections

- Create TbIotHubItemsPageComponent with hero section (title, description,
  illustration, installed items button, contribute link) per item type
- Add separate routes: /iot-hub/widgets, /iot-hub/dashboards,
  /iot-hub/solution-templates, /iot-hub/calculated-fields,
  /iot-hub/rule-chains, /iot-hub/devices
- Remove old /iot-hub/browse route, update all navigation links
- Browse component: add @Input activeType and hideTabs for embedded use
- Home component: navigateToBrowse now routes to type-specific pages
- Add item type page description translations
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
af38b95924
  1. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  2. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
  3. 22
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  4. 17
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  5. 52
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html
  6. 92
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss
  7. 115
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts
  8. 64
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  9. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts
  10. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html

@ -17,7 +17,7 @@
--> -->
<div class="tb-iot-hub-browse" [class.embedded]="embedded"> <div class="tb-iot-hub-browse" [class.embedded]="embedded">
<!-- Type tabs --> <!-- Type tabs -->
<nav mat-tab-nav-bar class="tb-iot-hub-tabs" [tabPanel]="tabPanel"> <nav mat-tab-nav-bar class="tb-iot-hub-tabs" [tabPanel]="tabPanel" [class.hidden]="hideTabs">
<a mat-tab-link <a mat-tab-link
[active]="activeType === ItemType.WIDGET" [active]="activeType === ItemType.WIDGET"
(click)="onTypeTabChange(ItemType.WIDGET)"> (click)="onTypeTabChange(ItemType.WIDGET)">

4
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss

@ -26,6 +26,10 @@
height: 100%; height: 100%;
} }
.hidden {
display: none !important;
}
// Tabs — Design: px-40, bottom border, labels auto-width (not stretched) // Tabs — Design: px-40, bottom border, labels auto-width (not stretched)
.tb-iot-hub-tabs { .tb-iot-hub-tabs {
padding: 0 40px; padding: 0 40px;

22
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts

@ -64,6 +64,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
@Input() creatorId: string; @Input() creatorId: string;
@Input() embedded = false; @Input() embedded = false;
@Input() hideTabs = false;
@Input() set activeType(value: ItemType) {
if (value && value !== this._activeType) {
this._activeType = value;
}
}
get activeType(): ItemType {
return this._activeType;
}
items: MpItemVersionView[] = []; items: MpItemVersionView[] = [];
totalElements = 0; totalElements = 0;
@ -73,7 +82,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
hasError = false; hasError = false;
textSearch = ''; textSearch = '';
activeType: ItemType = ItemType.WIDGET; _activeType: ItemType = ItemType.WIDGET;
activeCategories = new Set<string>(); activeCategories = new Set<string>();
activeUseCases = new Set<string>(); activeUseCases = new Set<string>();
activeCfTypes = new Set<string>(); activeCfTypes = new Set<string>();
@ -115,14 +124,9 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.pageIndex = 0; this.pageIndex = 0;
this.loadItems(); this.loadItems();
}); });
if (!this.embedded) { const params = this.route.snapshot.queryParams;
const params = this.route.snapshot.queryParams; if (params['search']) {
if (params['type'] && Object.values(ItemType).includes(params['type'])) { this.textSearch = params['search'];
this.activeType = params['type'] as ItemType;
}
if (params['search']) {
this.textSearch = params['search'];
}
} }
this.updateCategories(); this.updateCategories();
if (this.activeType === ItemType.WIDGET) { if (this.activeType === ItemType.WIDGET) {

17
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts

@ -159,12 +159,25 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
onSearch(): void { onSearch(): void {
if (this.searchText?.trim()) { if (this.searchText?.trim()) {
this.router.navigate(['/iot-hub/browse'], { queryParams: { search: this.searchText.trim() } }); this.router.navigate(['/iot-hub', this.getTypeRoute(this.activeHeroType?.type || ItemType.WIDGET)],
{ queryParams: { search: this.searchText.trim() } });
} }
} }
navigateToBrowse(type: ItemType): void { navigateToBrowse(type: ItemType): void {
this.router.navigate(['/iot-hub/browse'], { queryParams: { type } }); this.router.navigate(['/iot-hub', this.getTypeRoute(type)]);
}
private getTypeRoute(type: ItemType): string {
switch (type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'solution-templates';
case ItemType.CALCULATED_FIELD: return 'calculated-fields';
case ItemType.RULE_CHAIN: return 'rule-chains';
case ItemType.DEVICE: return 'devices';
default: return 'widgets';
}
} }
navigateToInstalledItems(): void { navigateToInstalledItems(): void {

52
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html

@ -0,0 +1,52 @@
<!--
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.
-->
<div class="tb-iot-hub-items-page">
<!-- Back link -->
<button mat-button color="primary" class="tb-iot-hub-back-btn" (click)="goBack()">
<mat-icon>chevron_left</mat-icon>
{{ 'iot-hub.back-to-iot-hub' | translate }}
</button>
<!-- Hero section -->
<div class="tb-iot-hub-items-hero">
<div class="tb-iot-hub-items-hero-text">
<h1 class="tb-iot-hub-items-title">{{ config.titleKey | translate }}</h1>
<p class="tb-iot-hub-items-desc">{{ config.descriptionKey | translate }}</p>
<div class="tb-iot-hub-items-actions">
@if (installedItemsCount > 0) {
<button mat-stroked-button color="primary" class="tb-iot-hub-items-installed-btn" (click)="navigateToInstalledItems()">
<mat-icon>inventory_2</mat-icon>
{{ 'iot-hub.installed-items' | translate }}: {{ installedItemsCount }}
</button>
}
<button mat-button color="primary" (click)="openSignup()">
{{ 'iot-hub.contribute' | translate }}
</button>
</div>
</div>
<div class="tb-iot-hub-items-hero-image">
<img [src]="config.image" alt="">
</div>
</div>
<!-- Browse component (without tabs) -->
<tb-iot-hub-browse
[activeType]="config.type"
[hideTabs]="true">
</tb-iot-hub-browse>
</div>

92
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss

@ -0,0 +1,92 @@
/**
* 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 "../../../../../scss/constants";
:host {
display: block;
padding: 16px;
}
.tb-iot-hub-items-page {
background: white;
border-radius: 8px;
padding: 40px 40px 0;
}
.tb-iot-hub-back-btn {
margin-bottom: 16px;
}
// Hero section
.tb-iot-hub-items-hero {
display: flex;
gap: 40px;
margin-bottom: 40px;
}
.tb-iot-hub-items-hero-text {
flex: 1;
min-width: 0;
}
.tb-iot-hub-items-title {
font-size: 40px;
font-weight: 500;
line-height: 48px;
color: rgba(0, 0, 0, 0.87);
margin: 0 0 12px;
}
.tb-iot-hub-items-desc {
font-size: 14px;
line-height: 20px;
color: rgba(0, 0, 0, 0.54);
margin: 0 0 24px;
max-width: 500px;
}
.tb-iot-hub-items-actions {
display: flex;
align-items: center;
gap: 16px;
}
.tb-iot-hub-items-installed-btn {
&.mat-primary {
--mat-button-outlined-outline-color: #{$tb-primary-color};
}
}
.tb-iot-hub-items-hero-image {
width: 400px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 200px;
object-fit: contain;
}
}
// Browse component — stretch to fill, ignore parent padding
tb-iot-hub-browse {
margin-left: -40px;
margin-right: -40px;
}

115
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts

@ -0,0 +1,115 @@
///
/// 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 { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
interface ItemTypePageConfig {
type: ItemType;
titleKey: string;
descriptionKey: string;
image: string;
routeSegment: string;
}
const PAGE_CONFIGS: Record<string, ItemTypePageConfig> = {
WIDGET: {
type: ItemType.WIDGET,
titleKey: 'item.type-widget-plural',
descriptionKey: 'iot-hub.items-page-desc-widgets',
image: 'assets/iot-hub/category-widgets-img.svg',
routeSegment: 'widgets'
},
DASHBOARD: {
type: ItemType.DASHBOARD,
titleKey: 'item.type-dashboard-plural',
descriptionKey: 'iot-hub.items-page-desc-dashboards',
image: 'assets/iot-hub/category-dashboards-img.svg',
routeSegment: 'dashboards'
},
SOLUTION_TEMPLATE: {
type: ItemType.SOLUTION_TEMPLATE,
titleKey: 'item.type-solution-template-plural',
descriptionKey: 'iot-hub.items-page-desc-solution-templates',
image: 'assets/iot-hub/category-solution-templates-img.png',
routeSegment: 'solution-templates'
},
CALCULATED_FIELD: {
type: ItemType.CALCULATED_FIELD,
titleKey: 'item.type-calculated-field-plural',
descriptionKey: 'iot-hub.items-page-desc-calculated-fields',
image: 'assets/iot-hub/category-calculated-fields-img.svg',
routeSegment: 'calculated-fields'
},
RULE_CHAIN: {
type: ItemType.RULE_CHAIN,
titleKey: 'item.type-rule-chain-plural',
descriptionKey: 'iot-hub.items-page-desc-rule-chains',
image: 'assets/iot-hub/category-rule-chains-img.svg',
routeSegment: 'rule-chains'
},
DEVICE: {
type: ItemType.DEVICE,
titleKey: 'iot-hub.device-library',
descriptionKey: 'iot-hub.items-page-desc-devices',
image: 'assets/iot-hub/category-device-library-img.svg',
routeSegment: 'devices'
}
};
@Component({
selector: 'tb-iot-hub-items-page',
standalone: false,
templateUrl: './iot-hub-items-page.component.html',
styleUrls: ['./iot-hub-items-page.component.scss']
})
export class TbIotHubItemsPageComponent implements OnInit {
config: ItemTypePageConfig;
installedItemsCount = 0;
constructor(
private route: ActivatedRoute,
private router: Router,
private iotHubApiService: IotHubApiService
) {}
ngOnInit(): void {
const itemType = this.route.snapshot.data['itemType'] as string;
this.config = PAGE_CONFIGS[itemType];
this.loadInstalledCount();
}
goBack(): void {
this.router.navigate(['/iot-hub']);
}
navigateToInstalledItems(): void {
this.router.navigate(['/iot-hub/installed']);
}
openSignup(): void {
window.open('https://iothub.thingsboard.io/signup', '_blank');
}
private loadInstalledCount(): void {
this.iotHubApiService.getInstalledItemsCount(this.config.type, { ignoreLoading: true }).subscribe(count => {
this.installedItemsCount = count;
});
}
}

64
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts

@ -19,7 +19,7 @@ import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubHomeComponent } from './iot-hub-home.component';
import { TbIotHubBrowseComponent } from './iot-hub-browse.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component';
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
@ -43,15 +43,63 @@ const routes: Routes = [
} }
}, },
{ {
path: 'browse', path: 'widgets',
component: TbIotHubBrowseComponent, component: TbIotHubItemsPageComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.browse', title: 'item.type-widget-plural',
breadcrumb: { itemType: 'WIDGET',
label: 'iot-hub.browse', breadcrumb: { label: 'item.type-widget-plural', icon: 'widgets' }
icon: 'search' }
} },
{
path: 'dashboards',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'item.type-dashboard-plural',
itemType: 'DASHBOARD',
breadcrumb: { label: 'item.type-dashboard-plural', icon: 'dashboard' }
}
},
{
path: 'solution-templates',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'item.type-solution-template-plural',
itemType: 'SOLUTION_TEMPLATE',
breadcrumb: { label: 'item.type-solution-template-plural', icon: 'integration_instructions' }
}
},
{
path: 'calculated-fields',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'item.type-calculated-field-plural',
itemType: 'CALCULATED_FIELD',
breadcrumb: { label: 'item.type-calculated-field-plural', icon: 'functions' }
}
},
{
path: 'rule-chains',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'item.type-rule-chain-plural',
itemType: 'RULE_CHAIN',
breadcrumb: { label: 'item.type-rule-chain-plural', icon: 'account_tree' }
}
},
{
path: 'devices',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.device-library',
itemType: 'DEVICE',
breadcrumb: { label: 'iot-hub.device-library', icon: 'memory' }
} }
}, },
{ {

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts

@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { IotHubRoutingModule } from './iot-hub-routing.module'; import { IotHubRoutingModule } from './iot-hub-routing.module';
import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubHomeComponent } from './iot-hub-home.component';
import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component';
import { TbIotHubBrowseComponent } from './iot-hub-browse.component'; import { TbIotHubBrowseComponent } from './iot-hub-browse.component';
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component';
import { TbIotHubItemDetailDialogComponent } from './iot-hub-item-detail-dialog.component'; import { TbIotHubItemDetailDialogComponent } from './iot-hub-item-detail-dialog.component';
@ -31,6 +32,7 @@ import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component
@NgModule({ @NgModule({
declarations: [ declarations: [
TbIotHubHomeComponent, TbIotHubHomeComponent,
TbIotHubItemsPageComponent,
TbIotHubBrowseComponent, TbIotHubBrowseComponent,
TbIotHubCreatorProfileComponent, TbIotHubCreatorProfileComponent,
TbIotHubItemDetailDialogComponent, TbIotHubItemDetailDialogComponent,

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

@ -3703,6 +3703,12 @@
"and-devices": "& Devices", "and-devices": "& Devices",
"search-placeholder": "Search in IoT Hub...", "search-placeholder": "Search in IoT Hub...",
"search": "Search...", "search": "Search...",
"items-page-desc-widgets": "Jumpstart your IoT journey with pre-configured widgets designed for your industry. Deploy proven architectures instantly and focus your energy on what matters: your unique business logic.",
"items-page-desc-dashboards": "Browse ready-to-use dashboards for monitoring, analytics, and device management. Deploy complete dashboard layouts instantly.",
"items-page-desc-solution-templates": "Complete IoT solution packages with dashboards, rule chains, and device configurations. Get started with proven architectures.",
"items-page-desc-calculated-fields": "Use pre-configured Calculated Fields to automate complex metrics like fuel efficiency or power factor. Skip the manual logic and keep your dashboards clean and actionable.",
"items-page-desc-rule-chains": "From sophisticated data processing to seamless API integrations, Rule Chain templates provide the architectural foundation you need to scale without building from scratch.",
"items-page-desc-devices": "Browse device configurations, profiles, and connectivity templates for sensors, gateways, and controllers.",
"device-library": "Device Library", "device-library": "Device Library",
"popular-widgets": "Popular Widgets", "popular-widgets": "Popular Widgets",
"popular-dashboards": "Popular Dashboards", "popular-dashboards": "Popular Dashboards",

Loading…
Cancel
Save