Browse Source

Responsive installed items, detail dialog, search, remove DEVICE from compact

Installed items:
- Sticky actions column with more_vert dropdown menu on lt-lg
- Scrollable table wrapper, column sizing with nowrap
- ResizeObserver hidePageSize at <640px
- Filter chips/clear on separate row below toolbar on lt-lg
- Responsive padding (24px lt-md, 16px xs), column header on xs

Item detail dialog:
- Responsive widths: 1200px default, 900px lt-xxl, 768px lt-lg, 100% lt-md
- Column layout for dlg-row-1 on lt-md, header on xs
- Custom carousel controls: arrows + dots on same row
- Carousel aspect-ratio 1.5 for solution templates
- isCompactLayout method (CF/RC only, no DEVICE)

Search page:
- Responsive padding (24px lt-lg, 16px lt-sm)
- Sort button color="primary", wraps on lt-sm
- Pagination column layout on lt-sm
- Results count below pagination on lt-lg

DEVICE removed from compact layout across all components:
- Item card, browse, detail dialog, home, search
- DEVICE now uses big card layout (same as widgets/dashboards)
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
a27bae710e
  1. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  2. 1
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  3. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  4. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  5. 39
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  6. 99
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss
  7. 12
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  8. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  9. 93
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html
  10. 131
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss
  11. 29
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  12. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html
  13. 49
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
  14. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts

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

@ -213,7 +213,7 @@
</button> </button>
} }
<span class="tb-iot-hub-sort-spacer"></span> <span class="tb-iot-hub-sort-spacer"></span>
<button mat-button class="tb-iot-hub-sort-btn" [matMenuTriggerFor]="sortMenu"> <button mat-button color="primary" class="tb-iot-hub-sort-btn" [matMenuTriggerFor]="sortMenu">
<mat-icon>filter_list</mat-icon> <mat-icon>filter_list</mat-icon>
{{ sortOptions[selectedSortIndex].label | translate }} {{ sortOptions[selectedSortIndex].label | translate }}
</button> </button>

1
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss

@ -304,7 +304,6 @@
// Sort button — Design: 14px Medium, primary color, with filter_list icon // Sort button — Design: 14px Medium, primary color, with filter_list icon
.tb-iot-hub-sort-btn { .tb-iot-hub-sort-btn {
color: $tb-primary-color;
flex-shrink: 0; flex-shrink: 0;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts

@ -74,7 +74,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
} }
get isCompactType(): boolean { get isCompactType(): boolean {
return this._activeType === ItemType.CALCULATED_FIELD || this._activeType === ItemType.RULE_CHAIN || this._activeType === ItemType.DEVICE; return this._activeType === ItemType.CALCULATED_FIELD || this._activeType === ItemType.RULE_CHAIN;
} }
items: MpItemVersionView[] = []; items: MpItemVersionView[] = [];

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts

@ -93,7 +93,7 @@ export class TbIotHubItemCardComponent {
} }
getCustomIconColor(): string | null { getCustomIconColor(): string | null {
if ((this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN) && this.item.color) { if (this.isCompactLayout() && this.item.color) {
return this.item.color; return this.item.color;
} }
return null; return null;

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

@ -18,7 +18,7 @@
<div class="tb-iot-hub-detail-dialog"> <div class="tb-iot-hub-detail-dialog">
<!-- Header --> <!-- Header -->
<div class="dlg-header"> <div class="dlg-header">
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) { @if (isCompactLayout()) {
<div class="dlg-header-icon" [style.background]="item.color || '#048ad3'"> <div class="dlg-header-icon" [style.background]="item.color || '#048ad3'">
<tb-icon>{{ getCompactIcon() }}</tb-icon> <tb-icon>{{ getCompactIcon() }}</tb-icon>
</div> </div>
@ -52,7 +52,7 @@
<div class="dlg-body"> <div class="dlg-body">
<!-- Main content --> <!-- Main content -->
<div class="dlg-main"> <div class="dlg-main">
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN || item.type === ItemType.DEVICE) { @if (isCompactLayout()) {
<!-- CF/RC/Device: description + author (no image) --> <!-- CF/RC/Device: description + author (no image) -->
<div class="dlg-info dlg-info-compact"> <div class="dlg-info dlg-info-compact">
<div class="dlg-info-text"> <div class="dlg-info-text">
@ -107,7 +107,7 @@
} @else { } @else {
<!-- Widget/Dashboard/Solution: image + description side by side --> <!-- Widget/Dashboard/Solution: image + description side by side -->
<div class="dlg-row-1"> <div class="dlg-row-1">
<div class="dlg-preview"> <div class="dlg-preview" [class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<div class="dlg-hero-carousel"> <div class="dlg-hero-carousel">
<ngx-hm-carousel <ngx-hm-carousel
@ -118,6 +118,7 @@
[between-delay]="0" [between-delay]="0"
[autoplay]="true" [autoplay]="true"
[mouse-enable]="true" [mouse-enable]="true"
[show-num]="1"
class="dlg-carousel"> class="dlg-carousel">
<section ngx-hm-carousel-container class="dlg-carousel-container"> <section ngx-hm-carousel-container class="dlg-carousel-container">
<article class="dlg-carousel-item" <article class="dlg-carousel-item"
@ -126,21 +127,25 @@
<img [src]="imageUrl" alt=""> <img [src]="imageUrl" alt="">
</article> </article>
</section> </section>
<ng-template #carouselPrev> <ng-template #carouselPrev><span class="dlg-carousel-hidden-nav"></span></ng-template>
<button mat-icon-button class="dlg-carousel-nav"> <ng-template #carouselNext><span class="dlg-carousel-hidden-nav"></span></ng-template>
<mat-icon>chevron_left</mat-icon> <ng-template #carouselDot><span></span></ng-template>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="dlg-carousel-dot"
[class.active]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel> </ngx-hm-carousel>
<div class="dlg-carousel-controls">
<button mat-icon-button class="dlg-carousel-nav" (click)="goToPrevSlide()">
<mat-icon>chevron_left</mat-icon>
</button>
<div class="dlg-carousel-dots">
@for (img of carouselImages; track img; let i = $index) {
<div class="dlg-carousel-dot"
[class.active]="i === carouselIndex"
(click)="carouselIndex = i"></div>
}
</div>
<button mat-icon-button class="dlg-carousel-nav" (click)="goToNextSlide()">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
</div> </div>
} @else { } @else {
@if (getPreviewUrl(); as url) { @if (getPreviewUrl(); as url) {

99
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -24,7 +24,24 @@
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
max-height: 80vh; max-height: 80vh;
width: 900px; width: 1200px;
@media #{$mat-lt-xxl} {
width: 900px;
}
@media #{$mat-lt-lg} {
width: 768px;
}
@media #{$mat-lt-md} {
width: 100%;
}
@media #{$mat-xs} {
max-height: 100%;
height: 100%;
}
} }
// Header — Design: pl-24, pr-12, py-16, gap between icon+content=16, content+close=8 // Header — Design: pl-24, pr-12, py-16, gap between icon+content=16, content+close=8
@ -135,6 +152,10 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
&.dlg-preview-carousel {
aspect-ratio: 1.5;
}
} }
.dlg-preview-img { .dlg-preview-img {
@ -399,6 +420,8 @@
.dlg-hero-carousel { .dlg-hero-carousel {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
animation: dlg-carousel-fade-in 0s 0.3s forwards; animation: dlg-carousel-fade-in 0s 0.3s forwards;
visibility: hidden; visibility: hidden;
} }
@ -413,17 +436,12 @@
::ng-deep .carousel { ::ng-deep .carousel {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
flex: 1;
> section { flex: 1; min-height: 0; } > section { flex: 1; min-height: 0; }
ul.indicators { ul.indicators { display: none !important; }
position: relative !important; .direction { display: none !important; }
bottom: auto !important;
flex-shrink: 0;
padding: 8px 0;
}
.direction { height: calc(100% - 40px) !important; }
} }
.dlg-carousel-container { .dlg-carousel-container {
@ -440,28 +458,50 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
padding: 8px;
} }
} }
} }
} }
.dlg-carousel-hidden-nav {
display: none;
}
.dlg-carousel-controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding-top: 16px;
}
.dlg-carousel-dots {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.dlg-carousel-nav { .dlg-carousel-nav {
color: rgba(0, 0, 0, 0.6); width: 40px;
background: rgba(255, 255, 255, 0.8); height: 40px;
background: white;
color: rgba(0, 0, 0, 0.76);
flex-shrink: 0;
&:hover { background: rgba(255, 255, 255, 0.95); } &:hover { background: rgba(245, 245, 245, 1); }
} }
.dlg-carousel-dot { .dlg-carousel-dot {
width: 8px; width: 6px;
height: 8px; height: 6px;
border-radius: 50%; border-radius: 999px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.38);
margin: 0 3px; margin: 9px 12px;
cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
&.active { background: rgba(0, 0, 0, 0.6); } &.active { background: rgba(0, 0, 0, 0.76); }
} }
// Info panel — Design: flex-col, space-between, py-16, gap-32 // Info panel — Design: flex-col, space-between, py-16, gap-32
@ -576,3 +616,24 @@
.dlg-footer-spacer { .dlg-footer-spacer {
flex: 1; flex: 1;
} }
// Responsive
@media #{$mat-lt-md} {
.dlg-row-1 {
flex-direction: column;
}
}
@media #{$mat-xs} {
.dlg-header {
flex-direction: column;
position: relative;
padding-right: 48px;
}
.dlg-close {
position: absolute;
top: 12px;
right: 12px;
}
}

12
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -90,6 +90,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
return translationKey ? this.translate.instant(translationKey) : key; return translationKey ? this.translate.instant(translationKey) : key;
} }
isCompactLayout(): boolean {
return this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN;
}
getPreviewUrl(): string | null { getPreviewUrl(): string | null {
return this.item.image ? this.iotHubApiService.resolveResourceUrl(this.item.image) : null; return this.item.image ? this.iotHubApiService.resolveResourceUrl(this.item.image) : null;
} }
@ -379,6 +383,14 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
this.dialogRef.close({ action: 'add', item: this.item }); this.dialogRef.close({ action: 'add', item: this.item });
} }
goToPrevSlide(): void {
this.carouselIndex = (this.carouselIndex - 1 + this.carouselImages.length) % this.carouselImages.length;
}
goToNextSlide(): void {
this.carouselIndex = (this.carouselIndex + 1) % this.carouselImages.length;
}
close(): void { close(): void {
this.dialogRef.close(); this.dialogRef.close();
} }

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

@ -254,7 +254,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
} }
isCompactType(type: ItemType): boolean { isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE; return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
} }
getCompactIcon(item: MpItemVersionView): string { getCompactIcon(item: MpItemVersionView): string {

93
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html

@ -39,37 +39,36 @@
<!-- Filter + Search row --> <!-- Filter + Search row -->
<div class="tb-installed-toolbar"> <div class="tb-installed-toolbar">
<div class="tb-installed-filter-area"> <button mat-stroked-button color="primary" class="tb-installed-filter-btn" [matMenuTriggerFor]="filterMenu">
<button mat-stroked-button color="primary" class="tb-installed-filter-btn" [matMenuTriggerFor]="filterMenu"> <mat-icon>filter_list</mat-icon>
<mat-icon>filter_list</mat-icon> {{ 'iot-hub.filter' | translate }}
{{ 'iot-hub.filter' | translate }} </button>
</button> <mat-menu #filterMenu="matMenu" class="tb-installed-filter-menu" xPosition="after">
<mat-menu #filterMenu="matMenu" class="tb-installed-filter-menu" xPosition="after"> @for (type of allItemTypes; track type; let last = $last) {
@for (type of allItemTypes; track type; let last = $last) { <div mat-menu-item class="tb-installed-filter-item" (click)="$event.stopPropagation(); toggleTypeFilter(type)">
<div mat-menu-item class="tb-installed-filter-item" (click)="$event.stopPropagation(); toggleTypeFilter(type)"> <mat-checkbox [checked]="isTypeFilterActive(type)">
<mat-checkbox [checked]="isTypeFilterActive(type)"> {{ getItemTypeLabel(type) }}
{{ getItemTypeLabel(type) }} </mat-checkbox>
</mat-checkbox> </div>
</div> @if (!last) {
@if (!last) { <mat-divider></mat-divider>
<mat-divider></mat-divider>
}
} }
</mat-menu>
@if (hasActiveFilters()) {
<mat-chip-set class="tb-installed-filter-chips">
@for (type of activeTypeFilters; track type) {
<mat-chip (removed)="removeTypeFilter(type)">
{{ getItemTypeLabel(type) }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
</mat-chip-set>
<button class="tb-installed-clear-btn" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all' | translate }}
</button>
} }
</div> </mat-menu>
@if (hasActiveFilters()) {
<mat-chip-set class="tb-installed-filter-chips">
@for (type of activeTypeFilters; track type) {
<mat-chip (removed)="removeTypeFilter(type)">
{{ getItemTypeLabel(type) }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
</mat-chip-set>
<button class="tb-installed-clear-btn" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all' | translate }}
</button>
}
<span class="tb-installed-toolbar-spacer"></span>
<div class="tb-installed-search"> <div class="tb-installed-search">
<mat-icon class="tb-installed-search-icon">search</mat-icon> <mat-icon class="tb-installed-search-icon">search</mat-icon>
<input class="tb-installed-search-input" <input class="tb-installed-search-input"
@ -81,6 +80,7 @@
<!-- Table + Paginator wrapped in border --> <!-- Table + Paginator wrapped in border -->
<div class="tb-installed-table-container"> <div class="tb-installed-table-container">
<div class="tb-installed-table-wrapper">
<table mat-table [dataSource]="dataSource" matSort matSortActive="createdTime" matSortDirection="desc"> <table mat-table [dataSource]="dataSource" matSort matSortActive="createdTime" matSortDirection="desc">
<!-- Name --> <!-- Name -->
<ng-container matColumnDef="itemName"> <ng-container matColumnDef="itemName">
@ -144,10 +144,14 @@
</ng-container> </ng-container>
<!-- Actions --> <!-- Actions -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef>
<div class="gt-md:!hidden" style="min-width: 48px;"></div>
<div class="lt-lg:!hidden" style="min-width: 144px;"></div>
</th>
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
<div class="tb-installed-actions"> <!-- Desktop: individual buttons -->
<div class="tb-installed-actions lt-lg:!hidden">
<button mat-icon-button (click)="viewItemDetails(item)" matTooltip="{{ 'iot-hub.view-item-details' | translate }}"> <button mat-icon-button (click)="viewItemDetails(item)" matTooltip="{{ 'iot-hub.view-item-details' | translate }}">
<mat-icon>info_outline</mat-icon> <mat-icon>info_outline</mat-icon>
</button> </button>
@ -158,12 +162,33 @@
<tb-icon>mdi:trash-can-outline</tb-icon> <tb-icon>mdi:trash-can-outline</tb-icon>
</button> </button>
</div> </div>
<!-- Mobile: dropdown menu -->
<div class="gt-md:!hidden">
<button mat-icon-button (click)="$event.stopPropagation()" [matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="viewItemDetails(item)">
<mat-icon>info_outline</mat-icon>
<span>{{ 'iot-hub.view-item-details' | translate }}</span>
</button>
<button mat-menu-item (click)="openEntity(item)">
<mat-icon>open_in_new</mat-icon>
<span>{{ (item.itemType === 'SOLUTION_TEMPLATE' ? 'iot-hub.goto-main-dashboard' : 'iot-hub.open-item-type') | translate:{ type: getItemTypeLabel(item.itemType) } }}</span>
</button>
<button mat-menu-item (click)="deleteItem(item)">
<tb-icon>mdi:trash-can-outline</tb-icon>
<span>{{ 'iot-hub.remove' | translate }}</span>
</button>
</mat-menu>
</div>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div>
@if (isLoading) { @if (isLoading) {
<div class="tb-installed-loading"> <div class="tb-installed-loading">
@ -178,11 +203,13 @@
} }
<!-- Paginator inside border --> <!-- Paginator inside border -->
<mat-divider></mat-divider>
<mat-paginator <mat-paginator
[length]="totalElements" [length]="totalElements"
[pageSize]="pageSize" [pageSize]="pageSize"
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSizeOptions]="[10, 25, 50]" [pageSizeOptions]="[10, 25, 50]"
[hidePageSize]="hidePageSize"
(page)="onPageChange($event)" (page)="onPageChange($event)"
showFirstLastButtons> showFirstLastButtons>
</mat-paginator> </mat-paginator>

131
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss

@ -19,17 +19,22 @@
:host { :host {
display: block; display: block;
padding: 16px; padding: 16px;
height: 100%;
} }
.tb-installed-items-page { .tb-installed-items-page {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
padding: 40px 40px 40px; padding: 40px 40px 40px;
height: 100%;
display: flex;
flex-direction: column;
} }
// Back button // Back button
.tb-installed-back-btn { .tb-installed-back-btn {
margin-bottom: 40px; margin-bottom: 40px;
align-self: flex-start;
} }
// Header: title + check for updates // Header: title + check for updates
@ -52,14 +57,12 @@
.tb-installed-toolbar { .tb-installed-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 8px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.tb-installed-filter-area { .tb-installed-toolbar-spacer {
display: flex; flex: 1;
align-items: center;
gap: 8px;
} }
.tb-installed-filter-btn { .tb-installed-filter-btn {
@ -151,6 +154,14 @@
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
.tb-installed-table-wrapper {
overflow: auto;
flex: 1;
table { table {
width: 100%; width: 100%;
@ -172,10 +183,55 @@
letter-spacing: 0.2px; letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76); color: rgba(0, 0, 0, 0.76);
} }
}
::ng-deep .mat-mdc-paginator {
border-top: none; // Column sizing
} .mat-column-itemName {
width: 50%;
min-width: 200px;
max-width: 50%;
}
.mat-column-itemType {
width: 50%;
min-width: 50%;
max-width: 50%;
}
.mat-column-version {
width: 80px;
min-width: 80px;
max-width: 80px;
}
.mat-column-createdTime {
width: 100px;
min-width: 100px;
max-width: 100px;
}
.mat-column-updates {
width: 250px;
min-width: 250px;
max-width: 250px;
}
.mat-column-actions {
width: 108px;
min-width: 108px;
max-width: 108px;
}
::ng-deep .mat-mdc-paginator-range-label {
white-space: nowrap;
}
// Nowrap columns
.mat-column-itemType,
.mat-column-createdTime,
.mat-column-updates {
white-space: nowrap;
} }
// Type chip — Design: pill, h-32, rounded-16, px-12 py-6, gap-4, bg at 6% opacity // Type chip — Design: pill, h-32, rounded-16, px-12 py-6, gap-4, bg at 6% opacity
@ -308,3 +364,62 @@
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
font-size: 14px; font-size: 14px;
} }
// Responsive
@media #{$mat-lt-lg} {
.tb-installed-toolbar {
flex-wrap: wrap;
}
.tb-installed-toolbar-spacer {
order: 0;
}
.tb-installed-search {
order: 1;
}
.tb-installed-filter-chips {
order: 10;
}
.tb-installed-clear-btn {
order: 11;
}
.tb-installed-toolbar::before {
content: '';
width: 100%;
order: 9;
}
.mat-column-actions {
width: 36px;
min-width: 36px;
max-width: 36px;
}
}
@media #{$mat-lt-md} {
.tb-installed-items-page {
padding: 40px 24px;
}
}
@media #{$mat-xs} {
.tb-installed-items-page {
padding-left: 16px;
padding-right: 16px;
}
.tb-installed-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.tb-installed-search {
width: 200px;
}
}

29
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core'; import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, ElementRef, NgZone, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
@ -43,12 +43,15 @@ import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from '@home/com
templateUrl: './iot-hub-installed-items.component.html', templateUrl: './iot-hub-installed-items.component.html',
styleUrls: ['./iot-hub-installed-items.component.scss'] styleUrls: ['./iot-hub-installed-items.component.scss']
}) })
export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit, OnDestroy {
displayedColumns: string[] = ['itemName', 'itemType', 'version', 'createdTime', 'updates', 'actions']; displayedColumns: string[] = ['itemName', 'itemType', 'version', 'createdTime', 'updates', 'actions'];
dataSource: IotHubInstalledItem[] = []; dataSource: IotHubInstalledItem[] = [];
totalElements = 0; totalElements = 0;
pageSize = 10; pageSize = 10;
hidePageSize = false;
private widgetResize$: ResizeObserver;
pageIndex = 0; pageIndex = 0;
isLoading = false; isLoading = false;
textSearch = ''; textSearch = '';
@ -74,8 +77,22 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
private store: Store<AppState>, private store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private dialog: MatDialog private dialog: MatDialog,
) {} private elementRef: ElementRef,
private zone: NgZone,
private cd: ChangeDetectorRef
) {
this.widgetResize$ = new ResizeObserver(() => {
this.zone.run(() => {
const shouldHide = this.elementRef.nativeElement.offsetWidth < 640;
if (shouldHide !== this.hidePageSize) {
this.hidePageSize = shouldHide;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
ngOnInit(): void { ngOnInit(): void {
const itemType = this.route.snapshot.queryParamMap.get('itemType'); const itemType = this.route.snapshot.queryParamMap.get('itemType');
@ -92,6 +109,10 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
this.loadData(); this.loadData();
} }
ngOnDestroy(): void {
this.widgetResize$?.disconnect();
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.sort.sortChange.subscribe(() => { this.sort.sortChange.subscribe(() => {
this.pageIndex = 0; this.pageIndex = 0;

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

@ -47,7 +47,7 @@
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
</button> </button>
</mat-form-field> </mat-form-field>
<button mat-button class="tb-search-sort-btn" [matMenuTriggerFor]="sortMenu"> <button mat-button color="primary" class="tb-search-sort-btn" [matMenuTriggerFor]="sortMenu">
<mat-icon>filter_list</mat-icon> <mat-icon>filter_list</mat-icon>
{{ sortOptions[selectedSortIndex].label | translate }} {{ sortOptions[selectedSortIndex].label | translate }}
</button> </button>

49
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss

@ -65,7 +65,6 @@
} }
.tb-search-sort-btn { .tb-search-sort-btn {
color: $tb-primary-color;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 20px; line-height: 20px;
@ -258,6 +257,29 @@
} }
@media #{$mat-lt-lg} { @media #{$mat-lt-lg} {
.tb-search-page {
padding: 24px;
}
.tb-search-input-field {
width: 400px;
}
.tb-search-pagination-row {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.tb-search-results-count {
order: 1;
}
.tb-search-pagination {
order: 0;
width: 100%;
}
.tb-search-card-grid { .tb-search-card-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -272,14 +294,33 @@
width: 100%; width: 100%;
flex: 1; flex: 1;
} }
}
@media #{$mat-lt-sm} {
.tb-search-page { .tb-search-page {
padding: 24px; padding: 16px;
}
.tb-search-toolbar {
flex-wrap: wrap;
}
.tb-search-sort-btn {
width: 100%;
justify-content: flex-start;
} }
}
@media #{$mat-lt-sm} {
.tb-search-card-grid { .tb-search-card-grid {
grid-template-columns: repeat(1, 1fr); grid-template-columns: repeat(1, 1fr);
} }
.tb-search-pagination {
flex-direction: column;
align-items: center;
gap: 16px;
}
.tb-search-page-size {
margin-left: 0;
}
} }

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

@ -39,8 +39,8 @@ interface SortOption {
} }
const TYPE_ORDER: ItemType[] = [ const TYPE_ORDER: ItemType[] = [
ItemType.WIDGET, ItemType.DASHBOARD, ItemType.SOLUTION_TEMPLATE, ItemType.WIDGET, ItemType.DASHBOARD, ItemType.SOLUTION_TEMPLATE, ItemType.DEVICE,
ItemType.CALCULATED_FIELD, ItemType.RULE_CHAIN, ItemType.DEVICE ItemType.CALCULATED_FIELD, ItemType.RULE_CHAIN
]; ];
@Component({ @Component({
@ -168,7 +168,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
// Type helpers // Type helpers
isCompactType(type: ItemType): boolean { isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE; return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
} }
getTypeLabel(type: ItemType): string { getTypeLabel(type: ItemType): string {

Loading…
Cancel
Save