From 00930b1f77db109e44a764681dc97a04ea9ed9cb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 Apr 2026 16:09:14 +0300 Subject: [PATCH] Redesign installed items page to match design - Back link + "Installed Items" title + "Check for updates" primary button - Filter button (stroked primary) with dropdown checkbox panel for item types - Filter chips (mat-chip-set) with "Clear all" button when active - Search field matching browse page design (320px, rounded-8, outlined) - Mat-table with columns: Name, Type (colored icon+text), Version, Installed date, Updates (up-to-date/available+update btn), Actions - Actions: info, open-in-new, remove (mdi:trash-can-outline) icon buttons - Updates column: green "Up to date" or "vX.X.X Available" + Update stroked warn - Item type filter passed as List to API - Use TbIotHubDeleteDialogComponent for remove confirmation --- .../iot-hub-installed-items.component.html | 200 +++++----- .../iot-hub-installed-items.component.scss | 351 +++++++++--------- .../iot-hub-installed-items.component.ts | 69 +++- .../assets/locale/locale.constant-en_US.json | 2 + 4 files changed, 347 insertions(+), 275 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html index 193a9f8d63..4fdc4ecf20 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html @@ -15,141 +15,171 @@ limitations under the License. --> -
- - arrow_back - {{ 'iot-hub.back-to-marketplace' | translate }} - -
-

{{ 'iot-hub.installed-items' | translate }}

- @if (!updatesChecked) { - - } +
+ + + + +
+

{{ 'iot-hub.installed-items' | translate }}

+
-
- - search - - @if (textSearch) { - + @if (hasActiveFilters()) { + + @for (type of activeTypeFilters; track type) { + + {{ getItemTypeLabel(type) }} + close + + } + + } - +
+
-
- @if (isLoading) { - - } + + @if (filterOpen) { +
+ @for (type of allItemTypes; track type) { + + {{ getItemTypeLabel(type) }} + + } +
+ } - + +
+
+ + - + + - + + + - + + - - - -
{{ 'iot-hub.item-name' | translate }} {{ item.itemName }} {{ 'iot-hub.item-type' | translate }}{{ 'iot-hub.item-type' | translate }} - {{ getItemTypeLabel(item.itemType) }} + + {{ getItemTypeIcon(item.itemType) }} + {{ getItemTypeLabel(item.itemType) }} + {{ 'iot-hub.item-version' | translate }}{{ 'iot-hub.item-version' | translate }} {{ item.version }} {{ 'iot-hub.installed-date' | translate }} {{ item.createdTime | date:'mediumDate' }} {{ updatesChecked ? ('iot-hub.updates' | translate) : '' }}{{ 'iot-hub.updates' | translate }} @if (updatesChecked) { - @if (getPublishedVersionInfo(item); as publishedInfo) { - @if (publishedInfo.publishedVersionId !== item.itemVersionId) { - - } @else { - {{ 'iot-hub.up-to-date' | translate }} + + check + {{ 'iot-hub.up-to-date' | translate }} + } } @else { - {{ 'iot-hub.up-to-date' | translate }} + + check + {{ 'iot-hub.up-to-date' | translate }} + } } - - @if (getEntityId(item)) { - + - } - + +
-
- {{ 'iot-hub.no-installed-items' | translate }} -
-
- - + @if (isLoading) { +
+ +
+ } + + @if (!isLoading && dataSource.length === 0) { +
+ {{ 'iot-hub.no-installed-items' | translate }} +
+ }
+ + + +
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss index 2897f9416a..d7e32bb9cc 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss @@ -14,246 +14,233 @@ * limitations under the License. */ +@import "../../../../../scss/constants"; + :host { - display: flex; - flex-direction: column; - height: 100%; - padding: 16px 24px; - overflow: auto; + display: block; + padding: 16px; } -.tb-iot-hub-installed-items-container { - max-width: 1400px; - margin: 0 auto; - width: 100%; +.tb-installed-items-page { background: white; - padding: 24px; - border-radius: 4px; - box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12); + border-radius: 8px; + padding: 40px 40px 24px; } -.tb-iot-hub-back-link { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 13px; - color: rgba(0, 0, 0, 0.54); - cursor: pointer; - text-decoration: none; - border-bottom: none; - margin-bottom: 12px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - - &:hover { - color: rgba(0, 0, 0, 0.87); - text-decoration: none; - border-bottom: none; - } +// Back button +.tb-installed-back-btn { + margin-bottom: 40px; } -.tb-iot-hub-installed-items-header { +// Header: title + check for updates +.tb-installed-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 24px; - - h2 { - font-size: 28px; - font-weight: 700; - margin: 0; - color: rgba(0, 0, 0, 0.87); - } + margin-bottom: 32px; } -.tb-iot-hub-inline-spinner { - display: inline-block; - margin-right: 8px; +.tb-installed-title { + font-size: 40px; + font-weight: 500; + line-height: 48px; + color: rgba(0, 0, 0, 0.87); + margin: 0; } -.tb-iot-hub-installed-search-row { +// Toolbar: filter + search +.tb-installed-toolbar { display: flex; - gap: 12px; - margin-bottom: 16px; align-items: center; + justify-content: space-between; + margin-bottom: 16px; } -.tb-iot-hub-installed-search-field { - flex: 1; - - ::ng-deep { - .mat-mdc-text-field-wrapper { - height: 42px; - padding: 0 12px; - } - - .mat-mdc-form-field-flex { - height: 42px; - align-items: center; - } - - .mdc-notched-outline__leading, - .mdc-notched-outline__notch, - .mdc-notched-outline__trailing { - border-color: #d5dbe3; - } - - .mdc-notched-outline__leading { - border-radius: 8px 0 0 8px; - } - - .mdc-notched-outline__trailing { - border-radius: 0 8px 8px 0; - } - - .mat-mdc-form-field-infix { - padding-top: 0; - padding-bottom: 0; - min-height: unset; - } +.tb-installed-filter-area { + display: flex; + align-items: center; + gap: 8px; +} - .mdc-notched-outline__notch { - display: none; - } +.tb-installed-filter-btn { + &.mat-primary { + --mat-button-outlined-outline-color: #{$tb-primary-color}; + } +} - .mat-mdc-floating-label { - display: none; - } +.tb-installed-filter-chips { + .mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) { + --mat-chip-elevated-container-color: rgba(0, 0, 0, 0.04); + } +} - .mat-mdc-form-field-subscript-wrapper { - display: none; - } +.tb-installed-clear-btn { + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.54); + cursor: pointer; + background: none; + border: none; + padding: 6px 8px; + font-family: inherit; - input.mat-mdc-input-element { - font-size: 14px; - color: #3e4a59; + &:hover { + color: rgba(0, 0, 0, 0.87); + } +} - &::placeholder { - color: #8b95a2; - opacity: 1; - } - } +// Search field +.tb-installed-search { + width: 320px; + height: 40px; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.12); + background: white; +} - .mat-icon { - color: #8b95a2; - font-size: 20px; - width: 20px; - height: 20px; - } +.tb-installed-search-icon { + font-size: 24px; + width: 24px; + height: 24px; + color: rgba(0, 0, 0, 0.38); + flex-shrink: 0; +} - .mat-mdc-form-field-icon-prefix { - padding: 0 8px 0 0; +.tb-installed-search-input { + flex: 1; + border: none; + outline: none; + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.87); + background: transparent; + font-family: inherit; - > .mat-icon { - padding: 0; - } - } + &::placeholder { + color: rgba(0, 0, 0, 0.38); + } +} - .mat-mdc-form-field-icon-suffix { - padding: 0; +// Filter dropdown +.tb-installed-filter-dropdown { + display: flex; + flex-direction: column; + width: 260px; + padding: 8px 0; + background: white; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 16px; - .mat-mdc-icon-button { - width: 32px; - height: 32px; - padding: 4px; - } - } - } + mat-checkbox { + display: block; + padding: 8px 16px; - &.mat-focused ::ng-deep { - .mdc-notched-outline__leading, - .mdc-notched-outline__notch, - .mdc-notched-outline__trailing { - border-color: #8b95a2; - border-width: 1px; + &:hover { + background: rgba(0, 0, 0, 0.04); } } } -.tb-iot-hub-installed-table-container { +// Table +.tb-installed-table-container { + border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px; overflow: hidden; table { width: 100%; } +} - .mat-column-itemName { - width: 100%; - } - - .mat-column-itemType { - width: 170px; - min-width: 170px; - max-width: 170px; - } +// Type cell with colored icon + text +.tb-installed-type { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 14px; + font-weight: 500; +} - .mat-column-version { - width: 100px; - min-width: 100px; - max-width: 100px; - } +.tb-installed-type-icon { + font-size: 16px; + width: 16px; + height: 16px; +} - .mat-column-createdTime { - width: 100px; - min-width: 100px; - max-width: 100px; - } +.tb-type-widget { color: #2e7d32; } +.tb-type-dashboard { color: #1565c0; } +.tb-type-solution-template { color: #2666a9; } +.tb-type-calc-field { color: #006d92; } +.tb-type-rule-chain { color: #95694b; } +.tb-type-device { color: #4b8a79; } - .mat-column-updates { - width: 200px; - min-width: 200px; - max-width: 200px; - } +// Updates column +.tb-installed-update-available { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: rgba(0, 0, 0, 0.76); + margin-right: 8px; - .mat-column-actions { - width: 144px; - min-width: 144px; - max-width: 144px; - text-align: right; - white-space: nowrap; + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; } } -.tb-iot-hub-type-chip { - padding: 2px 10px; - border-radius: 12px; +.tb-installed-update-btn { font-size: 12px; - font-weight: 500; - white-space: nowrap; - - &.tb-type-widget { background: #e3f2fd; color: #1565c0; } - &.tb-type-dashboard { background: #e8f5e9; color: #2e7d32; } - &.tb-type-calc-field { background: #fff3e0; color: #e65100; } - &.tb-type-rule-chain { background: #f3e5f5; color: #7b1fa2; } - &.tb-type-device { background: #eceff1; color: #455a64; } - &.tb-type-solution-template { background: #e0f7fa; color: #00695c; } } -.tb-iot-hub-up-to-date { - color: #2e7d32; +.tb-installed-up-to-date { + display: inline-flex; + align-items: center; + gap: 4px; font-size: 13px; - font-weight: 500; -} + color: #198038; -.tb-iot-hub-update-button { - font-size: 12px; - line-height: 28px; - height: 28px; - padding: 0 12px; + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } +} - & + .tb-iot-hub-update-button { - margin-left: 8px; +// Actions column +.tb-installed-actions { + display: flex; + align-items: center; + gap: 0; + justify-content: flex-end; + + .mat-mdc-icon-button { + width: 36px; + height: 36px; + padding: 6px; + --mdc-icon-button-state-layer-size: 36px; + --mdc-icon-button-icon-color: rgba(0, 0, 0, 0.54); + color: rgba(0, 0, 0, 0.54); } } -.tb-iot-hub-installed-empty { - padding: 48px 24px; +// Loading / Empty +.tb-installed-loading { + display: flex; + justify-content: center; + padding: 48px 0; +} + +.tb-installed-empty { text-align: center; + padding: 48px 0; color: rgba(0, 0, 0, 0.54); font-size: 14px; } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts index 940bfa5eee..c1b9f595f9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts @@ -35,6 +35,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { getEntityDetailsPageURL } from '@core/utils'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component'; +import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from './iot-hub-delete-dialog.component'; @Component({ selector: 'tb-iot-hub-installed-items', @@ -56,6 +57,11 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { updatesChecked = false; isCheckingUpdates = false; + // Type filter + filterOpen = false; + activeTypeFilters = new Set(); + allItemTypes: string[] = ['WIDGET', 'DASHBOARD', 'SOLUTION_TEMPLATE', 'CALCULATED_FIELD', 'RULE_CHAIN', 'DEVICE']; + private searchSubject = new Subject(); @ViewChild(MatSort, {static: true}) sort: MatSort; @@ -104,14 +110,60 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { this.loadData(); } + toggleFilter(): void { + this.filterOpen = !this.filterOpen; + } + + isTypeFilterActive(type: string): boolean { + return this.activeTypeFilters.has(type); + } + + toggleTypeFilter(type: string): void { + if (this.activeTypeFilters.has(type)) { + this.activeTypeFilters.delete(type); + } else { + this.activeTypeFilters.add(type); + } + this.pageIndex = 0; + this.loadData(); + } + + removeTypeFilter(type: string): void { + this.activeTypeFilters.delete(type); + this.pageIndex = 0; + this.loadData(); + } + + clearAllFilters(): void { + this.activeTypeFilters.clear(); + this.pageIndex = 0; + this.loadData(); + } + + hasActiveFilters(): boolean { + return this.activeTypeFilters.size > 0; + } + + getItemTypeIcon(itemType: string): string { + switch (itemType) { + case 'WIDGET': return 'widgets'; + case 'DASHBOARD': return 'dashboard'; + case 'SOLUTION_TEMPLATE': return 'integration_instructions'; + case 'CALCULATED_FIELD': return 'functions'; + case 'RULE_CHAIN': return 'settings_ethernet'; + case 'DEVICE': return 'memory'; + default: return 'category'; + } + } + deleteItem(item: IotHubInstalledItem): void { - this.dialogService.confirm( - this.translate.instant('iot-hub.remove-installed-item-title'), - this.translate.instant('iot-hub.remove-installed-item-text', {name: item.itemName}), - this.translate.instant('action.no'), - this.translate.instant('action.yes') - ).subscribe(result => { - if (result) { + const dialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, { + panelClass: ['tb-dialog'], + autoFocus: false, + data: { itemName: item.itemName, itemType: item.itemType } as IotHubDeleteDialogData + }); + dialogRef.afterClosed().subscribe(confirmed => { + if (confirmed) { this.iotHubApiService.deleteInstalledItem(item.id.id).subscribe({ next: () => { this.store.dispatch(new ActionNotificationShow({ @@ -260,7 +312,8 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { direction: this.sort?.direction === 'asc' ? Direction.ASC : Direction.DESC }; const pageLink = new PageLink(this.pageSize, this.pageIndex, this.textSearch || null, sortOrder); - this.iotHubApiService.getInstalledItems(pageLink, null, {ignoreLoading: true}).subscribe({ + const typeFilters = this.activeTypeFilters.size > 0 ? Array.from(this.activeTypeFilters) : null; + this.iotHubApiService.getInstalledItems(pageLink, typeFilters, {ignoreLoading: true}).subscribe({ next: (data) => { this.dataSource = data.data; this.totalElements = data.totalElements; 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 7119089cdd..7510611f2b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3705,6 +3705,8 @@ "search-placeholder": "Search in IoT Hub...", "search": "Search...", "items-per-page": "Items per page", + "filter": "Filter", + "available": "Available", "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": "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.",