Browse Source

Add responsive adaptiveness for IoT Hub home, browse, and search pages

- Add MediaBreakpoints lt-xmd (≤1599), gt-xxl (≥2448) to constants
- Home: BreakpointObserver dynamically adjusts card counts per breakpoint
- Home: main-content container with 48px gap and responsive padding
- Home: hero icons positions/sizes adapt per breakpoint, hidden below ≤959px
- Home: subtitle line break after Solution Templates
- Home: installed button centered at ≤599px
- Browse: responsive card grid with compact type support (CF/RC/DEVICE)
- Search: responsive grid columns matching home page breakpoints
- Card component: min-width 0 for grid overflow prevention
- isCompactType includes DEVICE across all components
pull/15508/head
Igor Kulikov 3 months ago
parent
commit
5a1cbe6781
  1. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  2. 42
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
  3. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  4. 252
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  5. 178
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
  6. 69
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  7. 1
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  8. 31
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
  9. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts
  10. 3
      ui-ngx/src/app/shared/models/constants.ts
  11. 2
      ui-ngx/src/scss/constants.scss

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

@ -202,7 +202,7 @@
<!-- Card grid -->
@if (!isLoading && !hasError && items.length > 0) {
<div class="tb-iot-hub-card-grid">
<div class="tb-iot-hub-card-grid" [class.tb-iot-hub-card-grid-compact]="isCompactType">
@for (item of items; track item.id) {
<tb-iot-hub-item-card
[item]="item"

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

@ -240,12 +240,16 @@
}
}
// Card grid Design: 4 columns, gap 20px
// Card grid Design: 4 columns, gap 20px; compact: 3 columns
.tb-iot-hub-card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 8px 0 24px;
&.tb-iot-hub-card-grid-compact {
grid-template-columns: repeat(3, 1fr);
}
}
// Loading / Empty states
@ -300,6 +304,7 @@
position: absolute;
left: 50%;
transform: translateX(-50%);
overflow: hidden;
.mat-mdc-icon-button {
width: 40px;
@ -365,18 +370,43 @@
}
// Responsive
@media (max-width: 1200px) {
@media #{$mat-gt-xxl} {
.tb-iot-hub-card-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(5, 1fr);
&.tb-iot-hub-card-grid-compact {
grid-template-columns: repeat(4, 1fr);
}
}
}
@media (max-width: 900px) {
.tb-iot-hub-filters {
width: 240px;
@media #{$mat-lt-xl} {
.tb-iot-hub-card-grid {
grid-template-columns: repeat(3, 1fr);
&.tb-iot-hub-card-grid-compact {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media #{$mat-lt-xmd} {
.tb-iot-hub-card-grid {
grid-template-columns: repeat(2, 1fr);
&.tb-iot-hub-card-grid-compact {
grid-template-columns: repeat(1, 1fr);
}
}
}
@media #{$mat-lt-lg} {
.tb-iot-hub-card-grid {
grid-template-columns: repeat(1, 1fr);
}
.tb-iot-hub-filters {
width: 240px;
}
}

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

@ -75,6 +75,10 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
return this._activeType;
}
get isCompactType(): boolean {
return this._activeType === ItemType.CALCULATED_FIELD || this._activeType === ItemType.RULE_CHAIN || this._activeType === ItemType.DEVICE;
}
items: MpItemVersionView[] = [];
totalElements = 0;
pageSize = 12;

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

@ -49,12 +49,15 @@
<h1 class="tb-iot-hub-hero-title">{{ 'iot-hub.home-title' | translate }}</h1>
<div class="tb-iot-hub-hero-subtitle">
<span class="tb-iot-hub-hero-prefix">{{ 'iot-hub.home-subtitle-prefix' | translate }}</span>
@for (ht of heroTypes; track ht.type; let last = $last) {
@for (ht of heroTypes; track ht.type; let i = $index; let last = $last) {
<span class="tb-iot-hub-hero-keyword"
[class.active]="activeHeroType === ht"
[style.--keyword-color]="ht.color"
(mouseenter)="onHeroTypeHover(ht)"
(mouseleave)="onHeroTypeLeave()">{{ ht.labelKey | translate }}@if (!last) {,}</span>
@if (i === 2) {
<span class="tb-iot-hub-hero-subtitle-break"></span>
}
}
</div>
<mat-form-field appearance="outline" class="tb-iot-hub-hero-search" subscriptSizing="dynamic" #searchField>
@ -131,134 +134,145 @@
</div>
</div>
<!-- Category cards -->
<div class="tb-iot-hub-categories">
@for (card of categoryCards; track card.type) {
<div class="tb-iot-hub-category-card" [ngClass]="card.cssClass" (click)="navigateToBrowse(card.type)">
<span class="tb-iot-hub-category-title">{{ card.titleKey | translate }}</span>
<img class="tb-iot-hub-category-img" [src]="card.image" alt="">
</div>
}
</div>
<!-- Main content -->
<div class="tb-iot-hub-main-content">
<!-- Category cards -->
<div class="tb-iot-hub-categories">
@for (card of categoryCards; track card.type) {
<div class="tb-iot-hub-category-card" [ngClass]="card.cssClass" (click)="navigateToBrowse(card.type)">
<span class="tb-iot-hub-category-title">{{ card.titleKey | translate }}</span>
<img class="tb-iot-hub-category-img" [src]="card.image" alt="">
</div>
}
</div>
@if (!isLoading) {
<!-- Popular Widgets -->
@if (popularWidgets.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.WIDGET)">
{{ 'iot-hub.popular-widgets' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularWidgets; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledWidget(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
@if (!isLoading) {
<!-- Popular Widgets -->
@if (popularWidgets.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.WIDGET)">
{{ 'iot-hub.popular-widgets' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularWidgets; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledWidget(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
</div>
}
}
<!-- Popular Dashboards -->
@if (popularDashboards.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DASHBOARD)">
{{ 'iot-hub.popular-dashboards' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularDashboards; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
<!-- Popular Dashboards -->
@if (popularDashboards.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DASHBOARD)">
{{ 'iot-hub.popular-dashboards' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularDashboards; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
</div>
}
}
<!-- Popular Solution Templates -->
@if (popularSolutionTemplates.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)">
{{ 'iot-hub.popular-solution-templates' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularSolutionTemplates; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledSolutionTemplate(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
<!-- Popular Solution Templates -->
@if (popularSolutionTemplates.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)">
{{ 'iot-hub.popular-solution-templates' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularSolutionTemplates; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledSolutionTemplate(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
</div>
}
}
<!-- Popular Calculated Fields -->
@if (popularCalcFields.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.CALCULATED_FIELD)">
{{ 'iot-hub.popular-calculated-fields' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularCalcFields; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
<!-- Popular Calculated Fields -->
@if (popularCalcFields.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.CALCULATED_FIELD)">
{{ 'iot-hub.popular-calculated-fields' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularCalcFields; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
</div>
}
}
<!-- Popular Rule Chains -->
@if (popularRuleChains.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)">
{{ 'iot-hub.popular-rule-chains' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularRuleChains; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
<!-- Popular Rule Chains -->
@if (popularRuleChains.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)">
{{ 'iot-hub.popular-rule-chains' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularRuleChains; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
}
@if (isLoading) {
<div class="tb-iot-hub-loading">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
</div>
<!-- Become a Creator -->
<!-- Become a Creator -->
@if (!isLoading) {
<div class="tb-iot-hub-divider"></div>
<div class="tb-iot-hub-become-creator">
<h2>{{ 'iot-hub.become-a-creator' | translate }}</h2>
@ -268,10 +282,4 @@
</button>
</div>
}
@if (isLoading) {
<div class="tb-iot-hub-loading">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
</div>

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

@ -102,6 +102,7 @@
}
// Individual floating icon SVG has rotation/bg/shadow baked in, CSS handles position + animation
// Base: 1600px 78px top icons (top: 40px), 87px bottom icons (top: 216px, bottom edge: 303px)
.tb-iot-hub-hero-float-icon {
position: absolute;
width: 78px;
@ -116,35 +117,37 @@
// Top-left
&.icon-pos-1 {
top: 40px;
left: 23%;
--icon-rotation: ;
left: calc(50% - 404px);
--start-x: 40px;
--start-y: 12px;
}
// Top-right
// Top-right mirrors icon-pos-1
&.icon-pos-2 {
top: 40px;
right: 23%;
--icon-rotation: ;
left: auto;
right: calc(50% - 404px);
--start-x: -40px;
--start-y: 12px;
}
// Bottom-left
// Bottom-left 87px bounding box
&.icon-pos-3 {
bottom: 100px;
left: 18%;
--icon-rotation: ;
top: 216px;
left: calc(50% - 487px);
width: 87px;
height: 87px;
--start-x: 50px;
--start-y: -8px;
}
// Bottom-right
// Bottom-right 87px bounding box, mirrors icon-pos-3
&.icon-pos-4 {
bottom: 100px;
right: 18%;
--icon-rotation: ;
top: 216px;
left: auto;
right: calc(50% - 487px);
width: 87px;
height: 87px;
--start-x: -45px;
--start-y: -4px;
}
@ -178,6 +181,11 @@
gap: 6px;
}
.tb-iot-hub-hero-subtitle-break {
flex-basis: 100%;
height: 0;
}
.tb-iot-hub-hero-prefix {
font-weight: 400;
color: rgba(0, 0, 0, 0.76);
@ -425,19 +433,26 @@
}
}
// Category cards Design: centered 1200px container, flex-col gap=20, pb=48, 3-col rows
// Main content wraps categories + item sections
.tb-iot-hub-main-content {
display: flex;
flex-direction: column;
gap: 48px;
padding: 0 40px 64px;
}
// Category cards Design: centered 1200px container, 3-col rows
.tb-iot-hub-categories {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding-bottom: 48px;
width: 100%;
}
// Card Design: flex-col, gap=24, pt=24, rounded-8, border, overflow-clip, h=220
.tb-iot-hub-category-card {
height: 220px;
border-radius: 8px;
cursor: pointer;
display: flex;
@ -470,8 +485,7 @@
// Card image Design: h=148, w=full, overflow-clip, fills remaining space
.tb-iot-hub-category-img {
width: 100%;
height: 148px;
object-fit: cover;
object-fit: contain;
flex-shrink: 0;
}
@ -500,10 +514,8 @@
background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%);
}
// Sections matches Design "items-section" (px=40, gap between sections ~48px)
// Sections inside main-content, no individual padding needed
.tb-iot-hub-section {
padding: 0 40px;
margin-top: 48px;
}
// Section header mat-button, Design: height=40, font-size=20, font-weight=500
@ -541,7 +553,6 @@
.tb-iot-hub-divider {
height: 1px;
background: rgba(0, 0, 0, 0.12);
margin: 48px 0 0;
}
// Become a Creator matches Design (pt=64, text centered, button at y=156)
@ -606,23 +617,41 @@
animation: iot-hub-fade-slide-up 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both;
}
// Responsive breakpoints from design: LG=1280, MD=960, SM=600, XS=375
// Responsive breakpoints from design: XXXL=2448, XXL=1920, XL=1600, LG=1280, MD=960, SM=600, XS=375
// LG: 1280px content area (sidebar present)
@media (max-width: 1600px) {
// 2448px: 6-col big cards, 4-col compact
@media #{$mat-gt-xxl} {
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(6, 1fr);
}
.tb-iot-hub-compact-cards-grid {
grid-template-columns: repeat(4, 1fr);
}
}
// 1919px: 4-col big cards, 3-col compact
@media #{$mat-lt-xl} {
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(4, 1fr);
}
}
// 1599px: 3-col big cards, 2-col compact
@media #{$mat-lt-xmd} {
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(3, 1fr);
}
.tb-iot-hub-compact-cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
// MD: 960px viewport sidebar present, narrower content
@media (max-width: 1280px) {
// 1279px: 2-col big cards, 1-col compact, 2-col categories, smaller icons
@media #{$mat-lt-lg} {
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
}
.tb-iot-hub-compact-cards-grid {
@ -631,14 +660,10 @@
.tb-iot-hub-categories {
grid-template-columns: repeat(2, 1fr);
.tb-iot-hub-category-card {
height: 189px;
}
}
.tb-iot-hub-hero {
padding: 100px 24px 60px;
padding: 140px 24px 60px;
}
.tb-iot-hub-hero-title {
@ -650,75 +675,102 @@
line-height: 22px;
}
.tb-iot-hub-section {
padding: 0 24px;
.tb-iot-hub-main-content {
padding: 0 24px 64px;
}
.tb-iot-hub-become-creator {
padding: 64px 24px;
}
}
// SM: 600px viewport sidebar hidden, full width
@media (max-width: 960px) {
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(2, 1fr);
}
// Icons: 1280px 69px top (top: 90px), 77px bottom (top: 246px, bottom edge: 323px)
.tb-iot-hub-hero-float-icon {
width: 69px;
height: 69px;
.tb-iot-hub-categories {
.tb-iot-hub-category-card {
height: 168px;
&.icon-pos-1 {
top: 90px;
left: 106px;
}
&.icon-pos-2 {
top: 90px;
left: auto;
right: 106px;
}
&.icon-pos-3 {
top: 246px;
left: 28px;
width: 77px;
height: 77px;
}
&.icon-pos-4 {
top: 246px;
left: auto;
right: 28px;
width: 77px;
height: 77px;
}
}
}
// 959px: only top pair, area width 160px (icon 51px + gap + icon 51px)
@media #{$mat-lt-md} {
.tb-iot-hub-hero-float-icon {
width: 51px;
height: 51px;
img {
width: 51px;
height: 51px;
&.icon-pos-1 {
top: 80px;
left: calc(50% - 80px);
}
&.icon-pos-2 {
top: 80px;
left: auto;
right: calc(50% - 80px);
}
&.icon-pos-3,
&.icon-pos-4 {
display: none;
}
}
}
// XS: 375px viewport single column, stacked layout
@media (max-width: 600px) {
// 599px: only top pair visible (51px), bottom pair hidden
@media #{$mat-lt-sm} {
:host {
padding: 8px;
}
.tb-iot-hub-hero {
padding: 80px 16px 40px;
padding: 140px 16px 40px;
}
.tb-iot-hub-categories {
grid-template-columns: 1fr;
padding-bottom: 32px;
.tb-iot-hub-category-card {
height: 197px;
}
}
.tb-iot-hub-big-cards-row {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: 1fr;
}
.tb-iot-hub-section {
padding: 0 16px;
.tb-iot-hub-main-content {
padding: 0 16px 48px;
}
.tb-iot-hub-become-creator {
padding: 48px 16px;
}
.tb-iot-hub-hero-float-icon {
display: none;
}
.tb-iot-hub-installed-btn {
top: 12px;
right: 12px;
right: auto;
left: 50%;
transform: translateX(-50%);
}
}

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

@ -19,8 +19,10 @@ import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { BreakpointObserver } from '@angular/cdk/layout';
import { forkJoin, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { MediaBreakpoints } from '@shared/models/constants';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
@ -134,15 +136,35 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
isLoading = true;
bigCardCount = 5;
compactCardCount = 6;
private breakpointSubscription: Subscription;
constructor(
private router: Router,
private dialog: MatDialog,
private iotHubApiService: IotHubApiService,
private translate: TranslateService
private translate: TranslateService,
private breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.updateCardCounts();
this.loadPopularItems();
this.breakpointSubscription = this.breakpointObserver.observe([
MediaBreakpoints['lt-sm'],
MediaBreakpoints['lt-md'],
MediaBreakpoints['lt-lg'],
MediaBreakpoints['lt-xmd'],
MediaBreakpoints['lt-xl'],
MediaBreakpoints['gt-xxl']
]).subscribe(() => {
const prev = { big: this.bigCardCount, compact: this.compactCardCount };
this.updateCardCounts();
if (this.bigCardCount !== prev.big || this.compactCardCount !== prev.compact) {
this.loadPopularItems();
}
});
this.searchSubscription = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
@ -169,6 +191,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stopHeroCycle();
this.searchSubscription?.unsubscribe();
this.breakpointSubscription?.unsubscribe();
}
onHeroTypeHover(config: HeroTypeConfig): void {
@ -236,7 +259,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
}
isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE;
}
getCompactIcon(item: MpItemVersionView): string {
@ -438,6 +461,38 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
window.open('https://iothub.thingsboard.io/signup', '_blank');
}
private updateCardCounts(): void {
if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-sm'])) {
// ≤599px: 1-col big cards, 1-col compact
this.bigCardCount = 2;
this.compactCardCount = 4;
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-md'])) {
// ≤959px: 2-col big cards, 1-col compact
this.bigCardCount = 4;
this.compactCardCount = 4;
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-lg'])) {
// ≤1279px: 2-col big cards, 1-col compact
this.bigCardCount = 4;
this.compactCardCount = 4;
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-xmd'])) {
// ≤1599px: 3-col big cards, 2-col compact
this.bigCardCount = 3;
this.compactCardCount = 4;
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-xl'])) {
// ≤1919px: 4-col big cards, 3-col compact
this.bigCardCount = 4;
this.compactCardCount = 6;
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-xxl'])) {
// ≥2448px: 6-col big cards, 4-col compact
this.bigCardCount = 6;
this.compactCardCount = 8;
} else {
// ≥1920px: 5-col big cards, 3-col compact
this.bigCardCount = 5;
this.compactCardCount = 6;
}
}
private loadPopularItems(): void {
const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC };
const config = { ignoreLoading: true };
@ -449,11 +504,11 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
};
forkJoin({
widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, 5), config),
dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, 5), config),
solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, 5), config),
calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config),
ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config),
widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, this.bigCardCount), config),
dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, this.bigCardCount), config),
solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, this.bigCardCount), config),
calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, this.compactCardCount), config),
ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, this.compactCardCount), config),
installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, config),
installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config),
installedCount: this.iotHubApiService.getInstalledItemsCount(null, config)

1
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss

@ -19,6 +19,7 @@
:host {
display: block;
height: 100%;
min-width: 0;
}
// Card Design: white bg, border rgba(0,0,0,0.12), rounded-8, overflow-clip, flex-col, gap-16, pb-16

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

@ -214,7 +214,7 @@
}
}
// Card grid Design: 5-column for big cards, 3-column for compact
// Card grid base: 1920 5-col big, 3-col compact
.tb-search-card-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
@ -225,18 +225,29 @@
}
}
// Responsive
@media (max-width: 1400px) {
// Responsive same columns as iot-hub home
@media #{$mat-gt-xxl} {
.tb-search-card-grid {
grid-template-columns: repeat(6, 1fr);
&.tb-search-card-grid-compact {
grid-template-columns: repeat(4, 1fr);
}
}
}
@media #{$mat-lt-xl} {
.tb-search-card-grid {
grid-template-columns: repeat(4, 1fr);
&.tb-search-card-grid-compact {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
}
}
}
@media (max-width: 1100px) {
@media #{$mat-lt-xmd} {
.tb-search-card-grid {
grid-template-columns: repeat(3, 1fr);
@ -246,7 +257,7 @@
}
}
@media (max-width: 900px) {
@media #{$mat-lt-lg} {
.tb-search-card-grid {
grid-template-columns: repeat(2, 1fr);
@ -254,7 +265,9 @@
grid-template-columns: repeat(1, 1fr);
}
}
}
@media #{$mat-lt-md} {
.tb-search-input-field {
width: 100%;
flex: 1;
@ -264,3 +277,9 @@
padding: 24px;
}
}
@media #{$mat-lt-sm} {
.tb-search-card-grid {
grid-template-columns: repeat(1, 1fr);
}
}

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

@ -173,7 +173,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
// Type helpers
isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE;
}
getTypeLabel(type: ItemType): string {

3
ui-ngx/src/app/shared/models/constants.ts

@ -69,11 +69,14 @@ export const MediaBreakpoints = {
'lt-sm': 'screen and (max-width: 599px)',
'lt-md': 'screen and (max-width: 959px)',
'lt-lg': 'screen and (max-width: 1279px)',
'lt-xmd': 'screen and (max-width: 1599px)',
'lt-xl': 'screen and (max-width: 1919px)',
'gt-xs': 'screen and (min-width: 600px)',
'gt-sm': 'screen and (min-width: 960px)',
'gt-md': 'screen and (min-width: 1280px)',
'gt-xmd': 'screen and (min-width: 1600px)',
'gt-lg': 'screen and (min-width: 1920px)',
'gt-xxl': 'screen and (min-width: 2448px)',
'gt-xl': 'screen and (min-width: 5001px)',
'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)'
};

2
ui-ngx/src/scss/constants.scss

@ -25,7 +25,9 @@ $mat-lt-xl: "screen and (max-width: 1919px)";
$mat-gt-xs: "screen and (min-width: 600px)";
$mat-gt-sm: "screen and (min-width: 960px)";
$mat-gt-md: "screen and (min-width: 1280px)";
$mat-lt-xmd: "screen and (max-width: 1599px)";
$mat-gt-xmd: "screen and (min-width: 1600px)";
$mat-gt-xxl: "screen and (min-width: 2448px)";
$mat-gt-xl: "screen and (min-width: 1920px)";
$mat-md-lg: "screen and (min-width: 960px) and (max-width: 1819px)";

Loading…
Cancel
Save