- Create TbIotHubHomeComponent as default route at /iot-hub - Move browse page to /iot-hub/browse, accept type/search query params - Interactive hero: hoverable keywords cycle through item types with color changes, background gradient transitions, and floating icon animations - Page load animation: hero and categories fade in with slide (down/up) - Category cards: 3x2 grid with exact Figma gradients and separated title/image layout - Popular sections: 5-column grid for widgets/dashboards/solutions, 3-column grid for calculated fields/rule chains - Become a Creator footer section - Export category card images and hero floating icons from Figma - Add plural item type translations and home page locale keyspull/15347/head
@ -0,0 +1,200 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2026 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-iot-hub-home"> |
|||
<!-- Hero section --> |
|||
<div class="tb-iot-hub-hero"> |
|||
<!-- Background gradient overlay --> |
|||
<div class="tb-iot-hub-hero-gradient" |
|||
[style.background]="'radial-gradient(ellipse at 50% 0%, ' + activeHeroType.gradientColor + ' 0%, transparent 60%)'"></div> |
|||
|
|||
<!-- Floating icons --> |
|||
@for (heroType of heroTypes; track heroType.type) { |
|||
@if (heroType.icons.length) { |
|||
<div class="tb-iot-hub-hero-icons" |
|||
[class.active]="heroIconsReady && activeHeroType === heroType"> |
|||
@for (iconUrl of heroType.icons; track iconUrl; let i = $index) { |
|||
<img class="tb-iot-hub-hero-float-icon" |
|||
[class]="'icon-pos-' + (i + 1)" |
|||
[src]="iconUrl" alt=""> |
|||
} |
|||
</div> |
|||
} |
|||
} |
|||
|
|||
<div class="tb-iot-hub-hero-content"> |
|||
<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) { |
|||
<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> |
|||
} |
|||
</div> |
|||
<mat-form-field appearance="outline" class="tb-iot-hub-hero-search"> |
|||
<input matInput |
|||
[placeholder]="'iot-hub.search-placeholder' | translate" |
|||
[(ngModel)]="searchText" |
|||
(keyup.enter)="onSearch()"> |
|||
<button matSuffix mat-icon-button (click)="onSearch()"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
</mat-form-field> |
|||
</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> |
|||
|
|||
@if (!isLoading) { |
|||
<!-- Popular Widgets --> |
|||
@if (popularWidgets.length) { |
|||
<div class="tb-iot-hub-section"> |
|||
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.WIDGET)"> |
|||
{{ 'iot-hub.popular-widgets' | translate }} |
|||
<mat-icon>chevron_right</mat-icon> |
|||
</a> |
|||
<div class="tb-iot-hub-big-cards-row"> |
|||
@for (item of popularWidgets; 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> |
|||
} |
|||
|
|||
<!-- Popular Dashboards --> |
|||
@if (popularDashboards.length) { |
|||
<div class="tb-iot-hub-section"> |
|||
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DASHBOARD)"> |
|||
{{ 'iot-hub.popular-dashboards' | translate }} |
|||
<mat-icon>chevron_right</mat-icon> |
|||
</a> |
|||
<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> |
|||
} |
|||
|
|||
<!-- Popular Solution Templates --> |
|||
@if (popularSolutionTemplates.length) { |
|||
<div class="tb-iot-hub-section"> |
|||
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)"> |
|||
{{ 'iot-hub.popular-solution-templates' | translate }} |
|||
<mat-icon>chevron_right</mat-icon> |
|||
</a> |
|||
<div class="tb-iot-hub-big-cards-row"> |
|||
@for (item of popularSolutionTemplates; 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> |
|||
} |
|||
|
|||
<!-- Popular Calculated Fields --> |
|||
@if (popularCalcFields.length) { |
|||
<div class="tb-iot-hub-section"> |
|||
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.CALCULATED_FIELD)"> |
|||
{{ 'iot-hub.popular-calculated-fields' | translate }} |
|||
<mat-icon>chevron_right</mat-icon> |
|||
</a> |
|||
<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> |
|||
} |
|||
|
|||
<!-- Popular Rule Chains --> |
|||
@if (popularRuleChains.length) { |
|||
<div class="tb-iot-hub-section"> |
|||
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)"> |
|||
{{ 'iot-hub.popular-rule-chains' | translate }} |
|||
<mat-icon>chevron_right</mat-icon> |
|||
</a> |
|||
<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> |
|||
} |
|||
|
|||
<!-- Become a Creator --> |
|||
<div class="tb-iot-hub-divider"></div> |
|||
<div class="tb-iot-hub-become-creator"> |
|||
<h2>{{ 'iot-hub.become-a-creator' | translate }}</h2> |
|||
<p>{{ 'iot-hub.become-creator-text' | translate }}</p> |
|||
<button mat-stroked-button color="primary" (click)="openSignup()"> |
|||
{{ 'iot-hub.submit-template' | translate }} |
|||
</button> |
|||
</div> |
|||
} |
|||
|
|||
@if (isLoading) { |
|||
<div class="tb-iot-hub-loading"> |
|||
<mat-spinner diameter="40"></mat-spinner> |
|||
</div> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,419 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
// Outer page — 16px padding around the white container |
|||
:host { |
|||
display: block; |
|||
padding: 16px; |
|||
} |
|||
|
|||
// Page container — white card with 8px radius, matches Figma "page-container" |
|||
.tb-iot-hub-home { |
|||
position: relative; |
|||
background: white; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
|
|||
// Dot grid pattern — covers top 800px, fades out via gradient mask |
|||
&::before { |
|||
content: ''; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 800px; |
|||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.1) 1px, transparent 1px); |
|||
background-size: 20px 20px; |
|||
mask-image: linear-gradient(to bottom, black 0%, transparent 100%); |
|||
-webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%); |
|||
pointer-events: none; |
|||
z-index: 0; |
|||
} |
|||
|
|||
> * { |
|||
position: relative; |
|||
z-index: 1; |
|||
} |
|||
} |
|||
|
|||
// Hero section |
|||
.tb-iot-hub-hero { |
|||
text-align: center; |
|||
padding: 140px 40px 80px; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
// Animated background gradient |
|||
.tb-iot-hub-hero-gradient { |
|||
position: absolute; |
|||
inset: 0; |
|||
transition: background 0.6s ease; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
// Floating icons container per type |
|||
.tb-iot-hub-hero-icons { |
|||
position: absolute; |
|||
inset: 0; |
|||
pointer-events: none; |
|||
|
|||
// Icons start near center (translate toward center, scaled down, invisible) |
|||
.tb-iot-hub-hero-float-icon { |
|||
opacity: 0; |
|||
transform: var(--icon-rotation) translate(var(--start-x), var(--start-y)) scale(0.9); |
|||
transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); |
|||
} |
|||
|
|||
// Active: icons move outward to final position, scale to 1, fully visible |
|||
&.active .tb-iot-hub-hero-float-icon { |
|||
opacity: 1; |
|||
transform: var(--icon-rotation) translate(0, 0) scale(1); |
|||
} |
|||
} |
|||
|
|||
// Individual floating icon — final (resting) positions match Figma |
|||
.tb-iot-hub-hero-float-icon { |
|||
position: absolute; |
|||
width: 64px; |
|||
height: 64px; |
|||
|
|||
// Top-left: moves from center-right and center-down to final pos |
|||
&.icon-pos-1 { |
|||
top: 40px; |
|||
left: 23%; |
|||
--icon-rotation: rotate(-15deg); |
|||
--start-x: 40px; |
|||
--start-y: 12px; |
|||
} |
|||
|
|||
// Top-right: moves from center-left and center-down |
|||
&.icon-pos-2 { |
|||
top: 40px; |
|||
right: 23%; |
|||
--icon-rotation: rotate(15deg); |
|||
--start-x: -40px; |
|||
--start-y: 12px; |
|||
} |
|||
|
|||
// Bottom-left: moves from center-right and center-up |
|||
&.icon-pos-3 { |
|||
bottom: 100px; |
|||
left: 18%; |
|||
--icon-rotation: rotate(-30deg); |
|||
--start-x: 50px; |
|||
--start-y: -8px; |
|||
} |
|||
|
|||
// Bottom-right: moves from center-left and center-up |
|||
&.icon-pos-4 { |
|||
bottom: 100px; |
|||
right: 18%; |
|||
--icon-rotation: rotate(30deg); |
|||
--start-x: -45px; |
|||
--start-y: -4px; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-hero-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 40px; |
|||
position: relative; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.tb-iot-hub-hero-title { |
|||
font-size: 56px; |
|||
font-weight: 500; |
|||
line-height: 1.2; |
|||
letter-spacing: 0.14px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
margin: 0; |
|||
} |
|||
|
|||
.tb-iot-hub-hero-subtitle { |
|||
font-size: 20px; |
|||
line-height: 24px; |
|||
letter-spacing: 0.1px; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
.tb-iot-hub-hero-prefix { |
|||
font-weight: 400; |
|||
color: rgba(0, 0, 0, 0.76); |
|||
} |
|||
|
|||
.tb-iot-hub-hero-keyword { |
|||
font-weight: 600; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
opacity: 0.48; |
|||
cursor: pointer; |
|||
transition: color 0.3s ease, opacity 0.3s ease; |
|||
|
|||
&.active { |
|||
color: var(--keyword-color); |
|||
opacity: 1; |
|||
} |
|||
|
|||
&:hover { |
|||
opacity: 0.8; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-hero-search { |
|||
width: 600px; |
|||
max-width: 100%; |
|||
|
|||
::ng-deep .mat-mdc-text-field-wrapper { |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: |
|||
0 7px 16px 0 rgba(0, 0, 0, 0.04), |
|||
0 29px 29px 0 rgba(0, 0, 0, 0.03), |
|||
0 64px 38px 0 rgba(0, 0, 0, 0.02); |
|||
} |
|||
|
|||
.mat-mdc-form-field-subscript-wrapper { |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
// Category cards — Figma: centered 1200px container, flex-col gap=20, pb=48, 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; |
|||
} |
|||
|
|||
// Card — Figma: 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; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 24px; |
|||
padding-top: 24px; |
|||
transition: transform 0.2s, box-shadow 0.2s; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
overflow: hidden; |
|||
|
|||
&:hover { |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); |
|||
} |
|||
} |
|||
|
|||
// Title — Figma: 18px Medium, centered, tracking=0.15, flex child |
|||
.tb-iot-hub-category-title { |
|||
font-size: 18px; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.15px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
text-align: center; |
|||
width: 100%; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
// Card image — Figma: h=148, w=full, overflow-clip, fills remaining space |
|||
.tb-iot-hub-category-img { |
|||
width: 100%; |
|||
height: 148px; |
|||
object-fit: cover; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
// Exact Figma gradients (104.75deg angle) |
|||
.category-widgets { |
|||
background: linear-gradient(104.75deg, rgb(240, 255, 245) 0%, rgb(147, 246, 182) 100%); |
|||
} |
|||
|
|||
.category-dashboards { |
|||
background: linear-gradient(104.75deg, rgb(245, 246, 255) 0%, rgb(189, 197, 255) 100%); |
|||
} |
|||
|
|||
.category-solutions { |
|||
background: linear-gradient(104.75deg, rgb(245, 250, 255) 0%, rgb(149, 200, 255) 100%); |
|||
} |
|||
|
|||
.category-calc-fields { |
|||
background: linear-gradient(104.75deg, rgb(245, 252, 255) 0%, rgb(149, 222, 248) 100%); |
|||
} |
|||
|
|||
.category-rule-chains { |
|||
background: linear-gradient(104.75deg, rgb(255, 252, 245) 0%, rgb(255, 238, 194) 100%); |
|||
} |
|||
|
|||
.category-devices { |
|||
background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%); |
|||
} |
|||
|
|||
// Sections — matches Figma "items-section" (px=40, gap between sections ~48px) |
|||
.tb-iot-hub-section { |
|||
padding: 0 40px; |
|||
margin-top: 48px; |
|||
} |
|||
|
|||
// Section header — matches Figma "button" (height=40, font-size=20, font-weight=500) |
|||
.tb-iot-hub-section-header { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
line-height: 40px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
cursor: pointer; |
|||
text-decoration: none; |
|||
|
|||
mat-icon { |
|||
font-size: 20px; |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
|
|||
&:hover { |
|||
color: #00695c; |
|||
} |
|||
} |
|||
|
|||
// Big card row — matches Figma "cards-row" (5 cards, gap=20, mt=12 from header) |
|||
.tb-iot-hub-big-cards-row { |
|||
display: grid; |
|||
grid-template-columns: repeat(5, 1fr); |
|||
gap: 20px; |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
// Compact card grid — matches Figma "cards" (3 columns, row gap=20, col gap=20) |
|||
.tb-iot-hub-compact-cards-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(3, 1fr); |
|||
gap: 20px; |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
// Divider — matches Figma "divider" (1px, rgba(0,0,0,0.12)) |
|||
.tb-iot-hub-divider { |
|||
height: 1px; |
|||
background: rgba(0, 0, 0, 0.12); |
|||
margin: 48px 0 0; |
|||
} |
|||
|
|||
// Become a Creator — matches Figma (pt=64, text centered, button at y=156) |
|||
.tb-iot-hub-become-creator { |
|||
text-align: center; |
|||
padding: 64px 40px; |
|||
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
// Loading |
|||
.tb-iot-hub-loading { |
|||
display: flex; |
|||
justify-content: center; |
|||
padding: 80px 0; |
|||
} |
|||
|
|||
// Page load animation — Figma: Default→appear-first, Smart Animate 1.02s, Gentle easing |
|||
// Hero elements fade in + slide DOWN 20px; categories fade in + slide UP 20px |
|||
@keyframes iot-hub-fade-slide-down { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(-20px); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
@keyframes iot-hub-fade-slide-up { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(20px); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-hero-title, |
|||
.tb-iot-hub-hero-subtitle, |
|||
.tb-iot-hub-hero-search { |
|||
animation: iot-hub-fade-slide-down 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both; |
|||
} |
|||
|
|||
.tb-iot-hub-categories { |
|||
animation: iot-hub-fade-slide-up 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both; |
|||
} |
|||
|
|||
// Responsive |
|||
@media (max-width: 1200px) { |
|||
.tb-iot-hub-big-cards-row { |
|||
grid-template-columns: repeat(3, 1fr); |
|||
} |
|||
|
|||
.tb-iot-hub-compact-cards-grid { |
|||
grid-template-columns: repeat(2, 1fr); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 900px) { |
|||
.tb-iot-hub-categories { |
|||
grid-template-columns: repeat(2, 1fr); |
|||
|
|||
.tb-iot-hub-category-card { |
|||
height: 180px; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-big-cards-row { |
|||
grid-template-columns: repeat(2, 1fr); |
|||
} |
|||
|
|||
.tb-iot-hub-hero-title { |
|||
font-size: 36px; |
|||
} |
|||
|
|||
.tb-iot-hub-hero { |
|||
padding: 80px 24px 40px; |
|||
} |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
///
|
|||
/// Copyright © 2016-2026 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { forkJoin } from 'rxjs'; |
|||
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'; |
|||
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
|||
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'; |
|||
|
|||
interface CategoryCard { |
|||
type: ItemType; |
|||
titleKey: string; |
|||
icon: string; |
|||
cssClass: string; |
|||
image: string; |
|||
} |
|||
|
|||
interface HeroTypeConfig { |
|||
type: ItemType; |
|||
labelKey: string; |
|||
color: string; |
|||
gradientColor: string; |
|||
icons: string[]; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-home', |
|||
standalone: false, |
|||
templateUrl: './iot-hub-home.component.html', |
|||
styleUrls: ['./iot-hub-home.component.scss'] |
|||
}) |
|||
export class TbIotHubHomeComponent implements OnInit, OnDestroy { |
|||
|
|||
readonly ItemType = ItemType; |
|||
|
|||
searchText = ''; |
|||
|
|||
heroTypes: HeroTypeConfig[] = [ |
|||
{ |
|||
type: ItemType.WIDGET, labelKey: 'item.type-widget-plural', color: '#2c9755', |
|||
gradientColor: 'rgba(44, 151, 85, 0.1)', |
|||
icons: ['assets/iot-hub/hero-widget-icon-1.svg', 'assets/iot-hub/hero-widget-icon-2.svg', 'assets/iot-hub/hero-widget-icon-3.svg', 'assets/iot-hub/hero-widget-icon-4.svg'] |
|||
}, |
|||
{ |
|||
type: ItemType.DASHBOARD, labelKey: 'item.type-dashboard-plural', color: '#4d5fd0', |
|||
gradientColor: 'rgba(77, 95, 208, 0.1)', |
|||
icons: ['assets/iot-hub/hero-dashboard-icon-1.svg', 'assets/iot-hub/hero-dashboard-icon-2.svg', 'assets/iot-hub/hero-dashboard-icon-3.svg', 'assets/iot-hub/hero-dashboard-icon-4.svg'] |
|||
}, |
|||
{ |
|||
type: ItemType.SOLUTION_TEMPLATE, labelKey: 'item.type-solution-template-plural', color: '#2666a9', |
|||
gradientColor: 'rgba(38, 102, 169, 0.1)', icons: [] |
|||
}, |
|||
{ |
|||
type: ItemType.CALCULATED_FIELD, labelKey: 'item.type-calculated-field-plural', color: '#006d92', |
|||
gradientColor: 'rgba(0, 109, 146, 0.1)', icons: [] |
|||
}, |
|||
{ |
|||
type: ItemType.RULE_CHAIN, labelKey: 'item.type-rule-chain-plural', color: '#95694b', |
|||
gradientColor: 'rgba(149, 105, 75, 0.1)', icons: [] |
|||
}, |
|||
{ |
|||
type: ItemType.DEVICE, labelKey: 'iot-hub.and-devices', color: '#4b8a79', |
|||
gradientColor: 'rgba(75, 138, 121, 0.1)', icons: [] |
|||
} |
|||
]; |
|||
|
|||
activeHeroType: HeroTypeConfig = this.heroTypes[0]; |
|||
heroIconsReady = false; |
|||
private heroInterval: any; |
|||
|
|||
categoryCards: CategoryCard[] = [ |
|||
{ type: ItemType.WIDGET, titleKey: 'item.type-widget-plural', icon: 'widgets', cssClass: 'category-widgets', image: 'assets/iot-hub/category-widgets-img.svg' }, |
|||
{ type: ItemType.DASHBOARD, titleKey: 'item.type-dashboard-plural', icon: 'dashboard', cssClass: 'category-dashboards', image: 'assets/iot-hub/category-dashboards-img.svg' }, |
|||
{ type: ItemType.SOLUTION_TEMPLATE, titleKey: 'item.type-solution-template-plural', icon: 'integration_instructions', cssClass: 'category-solutions', image: 'assets/iot-hub/category-solution-templates-img.png' }, |
|||
{ type: ItemType.CALCULATED_FIELD, titleKey: 'item.type-calculated-field-plural', icon: 'functions', cssClass: 'category-calc-fields', image: 'assets/iot-hub/category-calculated-fields-img.svg' }, |
|||
{ type: ItemType.RULE_CHAIN, titleKey: 'item.type-rule-chain-plural', icon: 'account_tree', cssClass: 'category-rule-chains', image: 'assets/iot-hub/category-rule-chains-img.svg' }, |
|||
{ type: ItemType.DEVICE, titleKey: 'iot-hub.device-library', icon: 'memory', cssClass: 'category-devices', image: 'assets/iot-hub/category-device-library-img.svg' } |
|||
]; |
|||
|
|||
popularWidgets: MpItemVersionView[] = []; |
|||
popularDashboards: MpItemVersionView[] = []; |
|||
popularSolutionTemplates: MpItemVersionView[] = []; |
|||
popularCalcFields: MpItemVersionView[] = []; |
|||
popularRuleChains: MpItemVersionView[] = []; |
|||
|
|||
isLoading = true; |
|||
|
|||
constructor( |
|||
private router: Router, |
|||
private dialog: MatDialog, |
|||
private iotHubApiService: IotHubApiService |
|||
) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.loadPopularItems(); |
|||
// One-tick delay so Angular renders icons in hidden state first, then triggers transition
|
|||
requestAnimationFrame(() => { |
|||
this.heroIconsReady = true; |
|||
this.startHeroCycle(); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.stopHeroCycle(); |
|||
} |
|||
|
|||
onHeroTypeHover(config: HeroTypeConfig): void { |
|||
this.stopHeroCycle(); |
|||
this.activeHeroType = config; |
|||
} |
|||
|
|||
onHeroTypeLeave(): void { |
|||
this.startHeroCycle(); |
|||
} |
|||
|
|||
private startHeroCycle(): void { |
|||
this.stopHeroCycle(); |
|||
this.heroInterval = setInterval(() => { |
|||
const idx = this.heroTypes.indexOf(this.activeHeroType); |
|||
this.activeHeroType = this.heroTypes[(idx + 1) % this.heroTypes.length]; |
|||
}, 3000); |
|||
} |
|||
|
|||
private stopHeroCycle(): void { |
|||
if (this.heroInterval) { |
|||
clearInterval(this.heroInterval); |
|||
this.heroInterval = null; |
|||
} |
|||
} |
|||
|
|||
onSearch(): void { |
|||
if (this.searchText?.trim()) { |
|||
this.router.navigate(['/iot-hub/browse'], { queryParams: { search: this.searchText.trim() } }); |
|||
} |
|||
} |
|||
|
|||
navigateToBrowse(type: ItemType): void { |
|||
this.router.navigate(['/iot-hub/browse'], { queryParams: { type } }); |
|||
} |
|||
|
|||
navigateToInstalledItems(): void { |
|||
this.router.navigate(['/iot-hub/installed']); |
|||
} |
|||
|
|||
openItemDetail(item: MpItemVersionView): void { |
|||
this.dialog.open(TbIotHubItemDetailDialogComponent, { |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
autoFocus: false, |
|||
data: { |
|||
item, |
|||
iotHubApiService: this.iotHubApiService |
|||
} as IotHubItemDetailDialogData |
|||
}); |
|||
} |
|||
|
|||
installItem(item: MpItemVersionView): void { |
|||
this.dialog.open(TbIotHubInstallDialogComponent, { |
|||
panelClass: ['tb-dialog'], |
|||
data: { |
|||
item, |
|||
iotHubApiService: this.iotHubApiService |
|||
} as IotHubInstallDialogData |
|||
}); |
|||
} |
|||
|
|||
navigateToCreator(creatorId: string): void { |
|||
this.router.navigate(['/iot-hub/creator', creatorId]); |
|||
} |
|||
|
|||
openSignup(): void { |
|||
window.open('https://iothub.thingsboard.io/signup', '_blank'); |
|||
} |
|||
|
|||
private loadPopularItems(): void { |
|||
const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC }; |
|||
const config = { ignoreLoading: true }; |
|||
|
|||
const buildQuery = (type: ItemType, size: number): MpItemVersionQuery => { |
|||
const pageLink = new PageLink(size, 0, null, sortOrder); |
|||
return new MpItemVersionQuery(pageLink, type); |
|||
}; |
|||
|
|||
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) |
|||
}).subscribe({ |
|||
next: (results) => { |
|||
this.popularWidgets = results.widgets.data; |
|||
this.popularDashboards = results.dashboards.data; |
|||
this.popularSolutionTemplates = results.solutionTemplates.data; |
|||
this.popularCalcFields = results.calcFields.data; |
|||
this.popularRuleChains = results.ruleChains.data; |
|||
this.isLoading = false; |
|||
}, |
|||
error: () => { |
|||
this.isLoading = false; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |