diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html index 40fc2d01ca..09c181e8ed 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html @@ -220,13 +220,29 @@ - - +
+
+ + @for (p of getPageNumbers(); track p) { + + } + +
+
+ {{ 'iot-hub.items-per-page' | translate }}: + + @for (size of pageSizeOptions; track size) { + {{ size }} + } + +
+
} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss index df8e4db483..9dc81d368c 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss @@ -54,7 +54,7 @@ // Left sidebar filters — Design: 280px wide, right border, own scroll .tb-iot-hub-filters { - width: 280px; + width: 300px; flex-shrink: 0; border-right: 1px solid rgba(0, 0, 0, 0.12); position: relative; @@ -88,7 +88,7 @@ background: transparent; .mat-expansion-panel-header { - padding: 8px 16px 8px 40px; + padding: 8px 16px 8px 56px; height: 40px; min-height: 40px; } @@ -105,7 +105,7 @@ mat-checkbox { display: block; - padding: 0 0 0 28px; + padding: 0 0 0 44px; &:hover { background: rgba(0, 0, 0, 0.04); @@ -283,9 +283,85 @@ color: rgba(0, 0, 0, 0.2); } -// Pagination -mat-paginator { +// Pagination — Design: centered page numbers, items per page on the right +.tb-iot-hub-pagination { + display: flex; + align-items: center; + padding: 24px 0 0; flex-shrink: 0; + position: relative; +} + +// Page buttons — Design: h-40, px-16 py-8, rounded-4, gap-1 +.tb-iot-hub-pagination-pages { + display: flex; + align-items: center; + gap: 1px; + position: absolute; + left: 50%; + transform: translateX(-50%); + + .mat-mdc-icon-button { + width: 40px; + height: 40px; + padding: 8px; + --mdc-icon-button-state-layer-size: 40px; + } +} + +.tb-iot-hub-page-btn { + height: 40px; + min-width: 40px; + padding: 8px 16px; + border-radius: 4px; + border: none; + background: transparent; + color: rgba(0, 0, 0, 0.76); + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; + + &:hover:not(.active) { + background: rgba(0, 0, 0, 0.04); + } + + &.active { + background: $tb-primary-color; + color: white; + } +} + +// Items per page — Design: gap-8, 14px Regular, right-aligned +.tb-iot-hub-pagination-size { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + margin-left: auto; +} + +// Page size dropdown — Design: 77×40, border rgba(0,0,0,0.12), rounded-4, px-12 py-6 +.tb-iot-hub-page-size-select { + width: 77px; + height: 40px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 6px 12px; + + ::ng-deep .mat-mdc-select-trigger { + font-size: 14px; + } } // Responsive @@ -297,7 +373,7 @@ mat-paginator { @media (max-width: 900px) { .tb-iot-hub-filters { - width: 220px; + width: 240px; } .tb-iot-hub-card-grid { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts index 66d49aa14a..7c19cc96ee 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts @@ -82,6 +82,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { hasError = false; textSearch = ''; + pageSizeOptions = [12, 24, 48]; _activeType: ItemType = ItemType.WIDGET; activeCategories = new Set(); activeUseCases = new Set(); @@ -206,6 +207,38 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } + getTotalPages(): number { + return Math.ceil(this.totalElements / this.pageSize); + } + + getPageNumbers(): number[] { + const total = this.getTotalPages(); + const pages: number[] = []; + const maxVisible = 5; + let start = Math.max(0, this.pageIndex - Math.floor(maxVisible / 2)); + let end = Math.min(total, start + maxVisible); + if (end - start < maxVisible) { + start = Math.max(0, end - maxVisible); + } + for (let i = start; i < end; i++) { + pages.push(i); + } + return pages; + } + + goToPage(page: number): void { + if (page >= 0 && page < this.getTotalPages()) { + this.pageIndex = page; + this.loadItems(); + } + } + + onPageSizeChange(size: number): void { + this.pageSize = size; + this.pageIndex = 0; + this.loadItems(); + } + clearCategories(): void { this.activeCategories.clear(); this.pageIndex = 0; @@ -389,7 +422,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { } as IotHubItemDetailDialogData }); dialogRef.afterClosed().subscribe(result => { - if (result === 'installed' || result === 'deleted') { + if (result === 'installed' || result === 'updated' || result === 'deleted') { this.reloadInstalledItems(); } }); diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index 0d7c9e7fb9..b7a1eb9108 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -97,6 +97,7 @@ (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" (installClick)="installItem($event)" + (updateClick)="updateItem($event)" (deleteClick)="deleteInstalledItem($event)"> } @@ -143,6 +144,7 @@ (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" (installClick)="installItem($event)" + (updateClick)="updateItem($event)" (deleteClick)="deleteInstalledItem($event)"> } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 6997592125..74f9a58302 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -26,6 +26,7 @@ import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-it import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; +import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component'; import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from './iot-hub-delete-dialog.component'; interface CategoryCard { @@ -246,6 +247,30 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { }); } + updateItem(item: MpItemVersionView): void { + const installedItem = this.findInstalledItem(item); + if (!installedItem) { + return; + } + const dialogRef = this.dialog.open(TbIotHubUpdateDialogComponent, { + panelClass: ['tb-dialog'], + autoFocus: false, + data: { + installedItemId: installedItem.id.id, + itemName: item.name, + itemType: item.type, + version: item.version, + versionId: item.id, + iotHubApiService: this.iotHubApiService + } as IotHubUpdateDialogData + }); + dialogRef.afterClosed().subscribe(result => { + if (result === 'updated') { + this.reloadInstalledItems(item.type); + } + }); + } + navigateToCreator(creatorId: string): void { this.router.navigate(['/iot-hub/creator', creatorId]); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html index b396e73679..b268d51314 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.html @@ -48,4 +48,14 @@ [activeType]="config.type" [hideTabs]="true"> + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss index da70c9f466..be636fb1f2 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss @@ -91,3 +91,33 @@ tb-iot-hub-browse { margin-left: -40px; margin-right: -40px; } + +// Footer divider +.tb-iot-hub-items-divider { + height: 1px; + background: rgba(0, 0, 0, 0.12); + margin-top: 48px; + margin-left: -40px; + margin-right: -40px; +} + +// Become a Creator footer +.tb-iot-hub-items-footer { + text-align: center; + padding: 40px 0 0; + + h2 { + font-size: 24px; + font-weight: 500; + line-height: 32px; + color: rgba(0, 0, 0, 0.87); + margin: 0 0 8px; + } + + p { + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.54); + margin: 0 0 32px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts index 771d8f7ab1..4f6f0a87be 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts @@ -39,14 +39,14 @@ const PAGE_CONFIGS: Record = { type: ItemType.DASHBOARD, titleKey: 'item.type-dashboard-plural', descriptionKey: 'iot-hub.items-page-desc-dashboards', - image: 'assets/iot-hub/category-dashboards-img.svg', + image: 'assets/iot-hub/items-page-dashboards-hero.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', + image: 'assets/iot-hub/items-page-solution-templates-hero.png', routeSegment: 'solution-templates' }, CALCULATED_FIELD: { @@ -67,7 +67,7 @@ const PAGE_CONFIGS: Record = { type: ItemType.DEVICE, titleKey: 'iot-hub.device-library', descriptionKey: 'iot-hub.items-page-desc-devices', - image: 'assets/iot-hub/category-device-library-img.svg', + image: 'assets/iot-hub/items-page-devices-hero.svg', routeSegment: 'devices' } }; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.html new file mode 100644 index 0000000000..77932f9bd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.html @@ -0,0 +1,64 @@ + +
+ @switch (state) { + @case ('confirm') { +

{{ 'iot-hub.update-confirm-title' | translate:{ name: data.itemName } }}

+

{{ 'iot-hub.update-desc' | translate }}

+ } + @case ('updating') { +

{{ 'iot-hub.update-confirm-title' | translate:{ name: data.itemName } }}

+

{{ 'iot-hub.update-desc' | translate }}

+ } + @case ('success') { +

{{ 'iot-hub.update-success-title' | translate }}

+

{{ 'iot-hub.update-success-desc' | translate:{ name: data.itemName } }}

+ } + @case ('error') { +

{{ 'iot-hub.update-error-title' | translate }}

+

{{ 'iot-hub.update-error-message' | translate:{ name: data.itemName } }}

+
{{ errorMessage }}
+ } + } +
+ + @switch (state) { + @case ('confirm') { + + + } + @case ('updating') { + + + } + @case ('success') { + + @if (entityDetailsUrl) { + + } + } + @case ('error') { + + } + } + diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.scss new file mode 100644 index 0000000000..a7ad4a2d3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.scss @@ -0,0 +1,57 @@ +/** + * 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. + */ + +.tb-iot-hub-install-content { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px; +} + +.tb-iot-hub-install-title { + font-size: 20px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.87); + margin: 0; +} + +.tb-iot-hub-install-desc { + color: rgba(0, 0, 0, 0.54); + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + margin: 0; +} + +.tb-iot-hub-inline-spinner { + display: inline-block; + margin-right: 8px; +} + +.tb-iot-hub-error-details { + max-height: 200px; + overflow-y: auto; + padding: 12px; + background: #f5f5f5; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: rgba(0, 0, 0, 0.7); + white-space: pre-wrap; + word-break: break-word; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts index c495c7be46..9eee9e0e10 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts @@ -43,99 +43,8 @@ export type UpdateState = 'confirm' | 'updating' | 'success' | 'error'; @Component({ selector: 'tb-iot-hub-update-dialog', standalone: false, - template: ` - @switch (state) { - @case ('confirm') { -

{{ 'iot-hub.update-item-title' | translate }}

- -

{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}

-

{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}

-
- - - - - } - @case ('updating') { -

{{ 'iot-hub.update-item-title' | translate }}

- -

{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}

-

{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}

-
- - - - - } - @case ('success') { -

- check_circle - {{ 'iot-hub.update-success-title' | translate }} -

- -

{{ 'iot-hub.update-success-message' | translate:{ name: data.itemName, version: data.version } }}

-
- - - @if (entityDetailsUrl) { - - } - - } - @case ('error') { -

- error - {{ 'iot-hub.update-error-title' | translate }} -

- -

{{ 'iot-hub.update-error-message' | translate:{ name: data.itemName } }}

-
{{ errorMessage }}
-
- - - - } - } - `, - styles: [` - .tb-iot-hub-install-meta { - margin: 4px 0; - color: rgba(0, 0, 0, 0.54); - font-size: 14px; - } - .tb-iot-hub-inline-spinner { - display: inline-block; - margin-right: 8px; - } - .tb-iot-hub-result-icon { - vertical-align: middle; - margin-right: 8px; - } - .tb-iot-hub-success-icon { - color: #2e7d32; - } - .tb-iot-hub-error-icon { - color: #c62828; - } - .tb-iot-hub-error-details { - max-height: 200px; - overflow-y: auto; - padding: 12px; - background: #f5f5f5; - border-radius: 4px; - font-family: monospace; - font-size: 12px; - color: rgba(0, 0, 0, 0.7); - white-space: pre-wrap; - word-break: break-word; - margin-top: 8px; - } - `] + templateUrl: './iot-hub-update-dialog.component.html', + styleUrls: ['./iot-hub-update-dialog.component.scss'] }) export class TbIotHubUpdateDialogComponent { diff --git a/ui-ngx/src/assets/iot-hub/items-page-dashboards-hero.svg b/ui-ngx/src/assets/iot-hub/items-page-dashboards-hero.svg new file mode 100644 index 0000000000..c699714457 --- /dev/null +++ b/ui-ngx/src/assets/iot-hub/items-page-dashboards-hero.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/iot-hub/items-page-devices-hero.svg b/ui-ngx/src/assets/iot-hub/items-page-devices-hero.svg new file mode 100644 index 0000000000..cbe42a4707 --- /dev/null +++ b/ui-ngx/src/assets/iot-hub/items-page-devices-hero.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/iot-hub/items-page-solution-templates-hero.png b/ui-ngx/src/assets/iot-hub/items-page-solution-templates-hero.png new file mode 100644 index 0000000000..690c23f552 Binary files /dev/null and b/ui-ngx/src/assets/iot-hub/items-page-solution-templates-hero.png differ diff --git a/ui-ngx/src/assets/iot-hub/items-page-widgets-hero.svg b/ui-ngx/src/assets/iot-hub/items-page-widgets-hero.svg index 4423f6348a..84c4b37a2f 100644 --- a/ui-ngx/src/assets/iot-hub/items-page-widgets-hero.svg +++ b/ui-ngx/src/assets/iot-hub/items-page-widgets-hero.svg @@ -1,124 +1,138 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + - + - + - - + + - - + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7d0c1a49a9..082e5391d5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3703,12 +3703,13 @@ "and-devices": "& Devices", "search-placeholder": "Search in IoT Hub...", "search": "Search...", + "items-per-page": "Items per page", "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-dashboards": "Explore dashboards to find pre-built visualization templates and interactive UI layouts you can deploy in minutes to bring your IoT data to life.", "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.", + "items-page-desc-devices": "Explore the device library to find pre-configured connectivity templates you can deploy in minutes to connect your hardware instantly.", "device-library": "Device Library", "popular-widgets": "Popular Widgets", "popular-dashboards": "Popular Dashboards", @@ -3792,6 +3793,9 @@ "update": "Update", "update-item-title": "Update Item", "update-confirm": "Update '{{name}}' to version {{version}}?", + "update-confirm-title": "Update '{{name}}'?", + "update-desc": "Updating this will replace the current version with the latest and reconfigure the associated resources within your tenant", + "update-success-desc": "'{{name}}' has been updated successfully", "updating": "Updating...", "update-success-title": "Update Successful", "update-success-message": "'{{name}}' has been updated to version {{version}}.",