Browse Source
- 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 handlingpull/15508/head
6 changed files with 795 additions and 1 deletions
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue