Browse Source

Add IoT Hub search page with grouped results, pagination, and empty states

- New TbIotHubSearchComponent at /iot-hub/search with ?search= query param
- Results grouped by item type with section headers linking to type pages
- 5-column grid for widgets/dashboards, 3-column for calculated fields/rule chains
- Search input with clear button, sort dropdown, pagination with page numbers
- Empty state with search_off icon when no results found
- Reuses item card component with install/update/delete dialog handling
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
75561518a8
  1. 13
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  2. 148
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html
  3. 267
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
  4. 361
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts
  5. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts
  6. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

13
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts

@ -22,6 +22,7 @@ import { TbIotHubHomeComponent } from './iot-hub-home.component';
import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component';
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
import { TbIotHubSearchComponent } from './iot-hub-search.component';
const routes: Routes = [
{
@ -102,6 +103,18 @@ const routes: Routes = [
breadcrumb: { label: 'iot-hub.device-library', icon: 'memory' }
}
},
{
path: 'search',
component: TbIotHubSearchComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.search-results',
breadcrumb: {
label: 'iot-hub.search-results',
icon: 'search'
}
}
},
{
path: 'installed',
component: TbIotHubInstalledItemsComponent,

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

@ -0,0 +1,148 @@
<!--
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-search-page">
<!-- Back -->
<button mat-button color="primary" class="tb-search-back-btn" (click)="navigateBack()">
<mat-icon>chevron_left</mat-icon>
{{ 'iot-hub.back-to-iot-hub' | translate }}
</button>
<!-- Title -->
<h1 class="tb-search-title">
@if (searchText?.trim()) {
{{ 'iot-hub.search-results-for' | translate }}<span class="tb-search-title-query">'{{ searchText.trim() }}'</span>
} @else {
{{ 'iot-hub.search-results' | translate }}
}
</h1>
<!-- Search + Sort row -->
<div class="tb-search-toolbar">
<mat-form-field appearance="outline" class="tb-search-input-field">
<input matInput
[(ngModel)]="searchText"
(input)="onSearchInput()"
(keyup.enter)="onSearchEnter()">
@if (searchText) {
<button matSuffix mat-icon-button (click)="clearSearch()">
<mat-icon>close</mat-icon>
</button>
}
<button matSuffix mat-icon-button (click)="$event.stopPropagation(); onSearchEnter()">
<mat-icon>search</mat-icon>
</button>
</mat-form-field>
<button mat-button class="tb-search-sort-btn" [matMenuTriggerFor]="sortMenu">
<mat-icon>filter_list</mat-icon>
{{ sortOptions[selectedSortIndex].label | translate }}
</button>
<mat-menu #sortMenu="matMenu">
@for (option of sortOptions; track option.value; let i = $index) {
<button mat-menu-item (click)="onSortChange(i)">{{ option.label | translate }}</button>
}
</mat-menu>
</div>
<!-- Results count + Pagination (shown when results exist) -->
@if (!isLoading && totalElements > 0) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
<div class="tb-search-pagination">
<div class="tb-search-pagination-pages">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-search-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= totalPages - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-search-page-size">
<span>{{ 'iot-hub.items-per-page' | translate }}</span>
<select [ngModel]="pageSize" (ngModelChange)="onPageSizeChange($event)">
@for (size of pageSizeOptions; track size) {
<option [value]="size">{{ size }}</option>
}
</select>
</div>
</div>
</div>
}
<!-- No results count when 0 -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: 0 } }}
</span>
</div>
}
<!-- Loading -->
@if (isLoading) {
<div class="tb-search-loading">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
<!-- No results -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-empty">
<mat-icon class="tb-search-empty-icon">search_off</mat-icon>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.try-adjusting-search' | translate }}</p>
</div>
}
<!-- Results grouped by type -->
@if (!isLoading && resultGroups.length > 0) {
<div class="tb-search-categories">
@for (group of resultGroups; track group.type) {
<div class="tb-search-section">
<!-- Section header -->
<button mat-button class="tb-search-section-header" (click)="navigateToType(group.type)">
{{ getTypeLabel(group.type) }}
<mat-icon>chevron_right</mat-icon>
</button>
<!-- Card grid -->
<div class="tb-search-card-grid" [class.tb-search-card-grid-compact]="isCompactType(group.type)">
@for (item of group.items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[showTypeChip]="false"
[showSubtype]="false"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
</div>
}
</div>

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

@ -0,0 +1,267 @@
/**
* 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 "../../../../../scss/constants";
:host {
display: block;
padding: 16px;
}
.tb-search-page {
background: white;
border-radius: 8px;
padding: 40px;
display: flex;
flex-direction: column;
gap: 40px;
}
// Back button
.tb-search-back-btn {
align-self: flex-start;
margin-bottom: -20px;
}
// Title — Design: 28px Medium, black-primary
.tb-search-title {
font-size: 28px;
font-weight: 500;
line-height: 36px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
.tb-search-title-query {
color: rgba(0, 0, 0, 0.54);
}
// Search + Sort toolbar
.tb-search-toolbar {
display: flex;
align-items: center;
gap: 16px;
}
.tb-search-input-field {
width: 480px;
::ng-deep .mat-mdc-text-field-wrapper {
border-radius: 8px;
}
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
}
.tb-search-sort-btn {
color: $tb-primary-color;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: 0.25px;
margin-left: auto;
}
// Results count + Pagination row
.tb-search-pagination-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: -28px;
}
.tb-search-results-count {
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgba(0, 0, 0, 0.76);
}
.tb-search-pagination {
display: flex;
align-items: center;
gap: 24px;
}
.tb-search-pagination-pages {
display: flex;
align-items: center;
gap: 1px;
}
.tb-search-page-btn {
min-width: 40px;
height: 40px;
border: none;
background: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
cursor: pointer;
padding: 8px 16px;
font-family: inherit;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
&.active {
background: $tb-primary-color;
color: white;
}
}
.tb-search-page-size {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: rgba(0, 0, 0, 0.76);
select {
width: 77px;
height: 40px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
font-family: inherit;
background: white;
cursor: pointer;
}
}
// Loading
.tb-search-loading {
display: flex;
justify-content: center;
padding: 48px 0;
margin-top: -20px;
}
// Empty state — Design: centered, 100px icon, 20px title, 14px subtitle
.tb-search-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
margin-top: -20px;
}
.tb-search-empty-icon {
font-size: 100px;
width: 100px;
height: 100px;
color: rgba(0, 0, 0, 0.38);
margin-bottom: 20px;
}
.tb-search-empty h3 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0.1px;
color: rgba(0, 0, 0, 0.76);
margin: 0 0 8px;
}
.tb-search-empty p {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
margin: 0;
}
// Categories / sections
.tb-search-categories {
display: flex;
flex-direction: column;
gap: 48px;
margin-top: -20px;
}
// Section header — Design: 18px Medium, clickable, right chevron
.tb-search-section-header {
font-size: 18px;
font-weight: 500;
line-height: 24px;
color: rgba(0, 0, 0, 0.87);
padding: 0;
margin-bottom: 12px;
mat-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
}
// Card grid — Design: 5-column for big cards, 3-column for compact
.tb-search-card-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
&.tb-search-card-grid-compact {
grid-template-columns: repeat(3, 1fr);
}
}
// Responsive
@media (max-width: 1400px) {
.tb-search-card-grid {
grid-template-columns: repeat(4, 1fr);
&.tb-search-card-grid-compact {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 1100px) {
.tb-search-card-grid {
grid-template-columns: repeat(3, 1fr);
&.tb-search-card-grid-compact {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 900px) {
.tb-search-card-grid {
grid-template-columns: repeat(2, 1fr);
&.tb-search-card-grid-compact {
grid-template-columns: repeat(1, 1fr);
}
}
.tb-search-input-field {
width: 100%;
flex: 1;
}
.tb-search-page {
padding: 24px;
}
}

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

@ -0,0 +1,361 @@
///
/// 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 { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
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 { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-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';
import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component';
import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from './iot-hub-delete-dialog.component';
import { TbDeviceInstallDialogComponent, DeviceInstallDialogData } from './device-install-dialog/device-install-dialog.component';
interface SearchResultGroup {
type: ItemType;
items: MpItemVersionView[];
}
interface SortOption {
value: string;
label: string;
direction: Direction;
}
const TYPE_ORDER: ItemType[] = [
ItemType.WIDGET, ItemType.DASHBOARD, ItemType.SOLUTION_TEMPLATE,
ItemType.CALCULATED_FIELD, ItemType.RULE_CHAIN, ItemType.DEVICE
];
@Component({
selector: 'tb-iot-hub-search',
standalone: false,
templateUrl: './iot-hub-search.component.html',
styleUrls: ['./iot-hub-search.component.scss']
})
export class TbIotHubSearchComponent implements OnInit, OnDestroy {
readonly ItemType = ItemType;
searchText = '';
resultGroups: SearchResultGroup[] = [];
totalElements = 0;
isLoading = false;
pageSize = 15;
pageIndex = 0;
pageSizeOptions = [15, 30, 60];
sortOptions: SortOption[] = [
{ value: 'totalInstallCount', label: 'iot-hub.sort-most-installed', direction: Direction.DESC },
{ value: 'publishedTime', label: 'iot-hub.sort-newest', direction: Direction.DESC },
{ value: 'name', label: 'iot-hub.sort-name', direction: Direction.ASC }
];
selectedSortIndex = 0;
installedWidgets: IotHubInstalledItem[] = [];
installedSolutionTemplates: IotHubInstalledItem[] = [];
private searchSubject = new Subject<string>();
private searchSubscription: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private translate: TranslateService,
private iotHubApiService: IotHubApiService
) {}
ngOnInit(): void {
this.loadInstalledItems();
this.searchSubscription = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(text => {
this.isLoading = true;
this.pageIndex = 0;
return this.fetchResults(text);
})
).subscribe(result => {
this.applyResults(result.data, result.totalElements);
});
const search = this.route.snapshot.queryParamMap.get('search');
if (search) {
this.searchText = search;
}
this.loadResults();
}
ngOnDestroy(): void {
this.searchSubscription?.unsubscribe();
}
navigateBack(): void {
this.router.navigate(['/iot-hub']);
}
onSearchInput(): void {
this.searchSubject.next(this.searchText || '');
}
clearSearch(): void {
this.searchText = '';
this.loadResults();
}
onSearchEnter(): void {
this.loadResults();
}
onSortChange(index: number): void {
this.selectedSortIndex = index;
this.pageIndex = 0;
this.loadResults();
}
// Pagination
get totalPages(): number {
return Math.ceil(this.totalElements / this.pageSize) || 0;
}
getPageNumbers(): number[] {
const total = this.totalPages;
if (total <= 5) {
return Array.from({length: total}, (_, i) => i);
}
const pages: number[] = [];
let start = Math.max(0, this.pageIndex - 2);
let end = Math.min(total - 1, start + 4);
if (end - start < 4) {
start = Math.max(0, end - 4);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
}
goToPage(page: number): void {
if (page >= 0 && page < this.totalPages) {
this.pageIndex = page;
this.loadResults();
}
}
onPageSizeChange(size: number): void {
this.pageSize = size;
this.pageIndex = 0;
this.loadResults();
}
// Type helpers
isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
}
getTypeLabel(type: ItemType): string {
const key = itemTypeTranslations.get(type);
return key ? this.translate.instant(key + '-plural') : type;
}
getTypeRoute(type: ItemType): string {
switch (type) {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'solution-templates';
case ItemType.CALCULATED_FIELD: return 'calculated-fields';
case ItemType.RULE_CHAIN: return 'rule-chains';
case ItemType.DEVICE: return 'devices';
default: return 'widgets';
}
}
navigateToType(type: ItemType): void {
this.router.navigate(['/iot-hub', this.getTypeRoute(type)]);
}
// Installed items
getInstalledItem(item: MpItemVersionView): IotHubInstalledItem | undefined {
switch (item.type) {
case ItemType.WIDGET:
return this.installedWidgets.find(i => i.itemId === item.itemId);
case ItemType.SOLUTION_TEMPLATE:
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId);
default:
return undefined;
}
}
// Dialogs
openItemDetail(item: MpItemVersionView): void {
const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
autoFocus: false,
data: {
item,
iotHubApiService: this.iotHubApiService,
installedItem: this.getInstalledItem(item)
} as IotHubItemDetailDialogData
});
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed' || result === 'deleted' || result === 'updated') {
this.reloadInstalledItems();
}
});
}
installItem(item: MpItemVersionView): void {
if (item.type === ItemType.DEVICE) {
this.installDevice(item);
return;
}
const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, {
panelClass: ['tb-dialog'],
autoFocus: false,
data: { item, iotHubApiService: this.iotHubApiService } as IotHubInstallDialogData
});
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed') {
this.reloadInstalledItems();
}
});
}
private installDevice(item: MpItemVersionView): void {
this.iotHubApiService.getVersionFileData(item.id as string, { ignoreLoading: true }).subscribe({
next: async (blob: Blob) => {
const zipData = await blob.arrayBuffer();
const dialogRef = this.dialog.open(TbDeviceInstallDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
disableClose: true,
autoFocus: false,
data: { item, zipData, iotHubApiService: this.iotHubApiService } as DeviceInstallDialogData
});
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed') {
this.reloadInstalledItems();
}
});
}
});
}
updateItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; }
const dialogRef = this.dialog.open(TbIotHubUpdateDialogComponent, {
panelClass: ['tb-dialog'],
autoFocus: false,
data: {
installedItemId: installedItem.id.id,
itemName: item.name,
itemType: item.type,
version: item.version,
versionId: item.id,
iotHubApiService: this.iotHubApiService
} as IotHubUpdateDialogData
});
dialogRef.afterClosed().subscribe(result => {
if (result === 'updated') {
this.reloadInstalledItems();
}
});
}
deleteInstalledItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; }
const dialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, {
panelClass: ['tb-dialog'],
autoFocus: false,
data: { itemName: item.name, itemType: item.type } as IotHubDeleteDialogData
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.iotHubApiService.deleteInstalledItem(installedItem.id.id).subscribe(() => {
this.reloadInstalledItems();
});
}
});
}
navigateToCreator(creatorId: string): void {
this.router.navigate(['/iot-hub/creator', creatorId]);
}
// Data loading
private loadResults(): void {
this.isLoading = true;
this.fetchResults(this.searchText || '').subscribe(result => {
this.applyResults(result.data, result.totalElements);
});
}
private fetchResults(text: string) {
const sort = this.sortOptions[this.selectedSortIndex];
const sortOrder: SortOrder = { property: sort.value, direction: sort.direction };
const pageLink = new PageLink(this.pageSize, this.pageIndex, text.trim() || null, sortOrder);
const query = new MpItemVersionQuery(pageLink);
return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true });
}
private applyResults(data: MpItemVersionView[], totalElements: number): void {
this.totalElements = totalElements;
this.resultGroups = this.groupResults(data);
this.isLoading = false;
}
private groupResults(items: MpItemVersionView[]): SearchResultGroup[] {
const groupMap = new Map<ItemType, MpItemVersionView[]>();
for (const item of items) {
let list = groupMap.get(item.type);
if (!list) {
list = [];
groupMap.set(item.type, list);
}
list.push(item);
}
return TYPE_ORDER
.filter(type => groupMap.has(type))
.map(type => ({ type, items: groupMap.get(type) }));
}
private loadInstalledItems(): void {
const config = { ignoreLoading: true };
const pageLink = new PageLink(10000, 0);
forkJoin({
widgets: this.iotHubApiService.getInstalledItems(pageLink, ItemType.WIDGET, config),
solutionTemplates: this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config)
}).subscribe(results => {
this.installedWidgets = results.widgets.data;
this.installedSolutionTemplates = results.solutionTemplates.data;
});
}
private reloadInstalledItems(): void {
this.loadInstalledItems();
}
}

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

@ -29,6 +29,7 @@ import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.compo
import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component';
import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component';
import { TbDeviceInstallDialogComponent } from './device-install-dialog/device-install-dialog.component';
import { TbIotHubSearchComponent } from './iot-hub-search.component';
@NgModule({
declarations: [
@ -42,7 +43,8 @@ import { TbDeviceInstallDialogComponent } from './device-install-dialog/device-i
TbIotHubInstalledItemsComponent,
TbIotHubUpdateDialogComponent,
TbIotHubDeleteDialogComponent,
TbDeviceInstallDialogComponent
TbDeviceInstallDialogComponent,
TbIotHubSearchComponent
],
imports: [
CommonModule,

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3707,6 +3707,9 @@
"see-all-results": "See all results",
"no-search-results": "No results found for '{{ search }}'",
"no-items-available": "No items available",
"search-results": "Search results",
"search-results-for": "Search results for ",
"try-adjusting-search": "Try adjusting your search",
"items-per-page": "Items per page",
"filter": "Filter",
"available": "Available",

Loading…
Cancel
Save