@ -15,214 +15,210 @@
limitations under the License.
limitations under the License.
-->
-->
< div class = "tb-iot-hub-browse-container" [ class . embedded ] = " embedded " >
< div class = "tb-iot-hub-browse" [ class . embedded ] = " embedded " >
<!-- Page header -->
@if (!embedded) {
< div class = "tb-iot-hub-browse-header" >
< div >
< h1 > {{ getTitle() | translate }}< / h1 >
< p > {{ getSubtitle() | translate }}< / p >
< / div >
< button mat-stroked-button color = "primary" ( click ) = " navigateToInstalledItems ( ) " >
< mat-icon > inventory_2< / mat-icon >
{{ 'iot-hub.installed-items' | translate }}
< / button >
< / div >
}
<!-- Type tabs -->
<!-- Type tabs -->
< nav mat-tab-nav-bar class = "tb-iot-hub-type-t abs" [ tabPanel ] = " tabPanel " >
< nav mat-tab-nav-bar class = "tb-iot-hub-tabs" [ tabPanel ] = " tabPanel " >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.WIDGET"
[active]="activeType === ItemType.WIDGET"
(click)="onTypeTabChange(ItemType.WIDGET)">
(click)="onTypeTabChange(ItemType.WIDGET)">
{{ 'item.type-widget' | translate }}s
{{ 'item.type-widget-plural' | translate }}
< / a >
< / a >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.DASHBOARD"
[active]="activeType === ItemType.DASHBOARD"
(click)="onTypeTabChange(ItemType.DASHBOARD)">
(click)="onTypeTabChange(ItemType.DASHBOARD)">
{{ 'item.type-dashboard' | translate }}s
{{ 'item.type-dashboard-plural' | translate }}
< / a >
< / a >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.SOLUTION_TEMPLATE"
[active]="activeType === ItemType.SOLUTION_TEMPLATE"
(click)="onTypeTabChange(ItemType.SOLUTION_TEMPLATE)">
(click)="onTypeTabChange(ItemType.SOLUTION_TEMPLATE)">
{{ 'item.type-solution-template' | translate }}s
{{ 'item.type-solution-template-plural ' | translate }}
< / a >
< / a >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.CALCULATED_FIELD"
[active]="activeType === ItemType.CALCULATED_FIELD"
(click)="onTypeTabChange(ItemType.CALCULATED_FIELD)">
(click)="onTypeTabChange(ItemType.CALCULATED_FIELD)">
{{ 'item.type-calculated-field' | translate }}s
{{ 'item.type-calculated-field-plural ' | translate }}
< / a >
< / a >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.RULE_CHAIN"
[active]="activeType === ItemType.RULE_CHAIN"
(click)="onTypeTabChange(ItemType.RULE_CHAIN)">
(click)="onTypeTabChange(ItemType.RULE_CHAIN)">
{{ 'item.type-rule-chain' | translate }}s
{{ 'item.type-rule-chain-plural ' | translate }}
< / a >
< / a >
< a mat-tab-link
< a mat-tab-link
[active]="activeType === ItemType.DEVICE"
[active]="activeType === ItemType.DEVICE"
(click)="onTypeTabChange(ItemType.DEVICE)">
(click)="onTypeTabChange(ItemType.DEVICE)">
{{ 'item.type-device' | translate }}s
{{ 'item.type-device-plural ' | translate }}
< / a >
< / a >
< / nav >
< / nav >
< mat-tab-nav-panel # tabPanel > < / mat-tab-nav-panel >
< mat-tab-nav-panel # tabPanel > < / mat-tab-nav-panel >
<!-- Search + sort row -->
<!-- Content: filters sidebar + card grid -->
< div class = "tb-iot-hub-search-row" >
< div class = "tb-iot-hub-content" >
< mat-form-field appearance = "outline" class = "tb-iot-hub-search-field" >
<!-- Left sidebar filters -->
< mat-icon matPrefix > search< / mat-icon >
< div class = "tb-iot-hub-filters" >
< input matInput
< div class = "tb-iot-hub-filters-inner" >
[placeholder]="getSearchPlaceholder() | translate"
@if (getSubtypeMap(); as subtypeMap) {
[value]="textSearch"
< mat-expansion-panel [ expanded ] = " true " class = "tb-iot-hub-filter-panel" >
(input)="onSearchChange($any($event.target).value)">
< mat-expansion-panel-header >
@if (textSearch) {
< mat-panel-title > {{ 'iot-hub.type-filter-label' | translate }}< / mat-panel-title >
< button matSuffix mat-icon-button ( click ) = " onSearchChange ( ' ' ) " >
< / mat-expansion-panel-header >
< mat-icon > close< / mat-icon >
@for (entry of subtypeMap | keyvalue; track entry.key) {
< / button >
< mat-checkbox [ checked ] = " isSubtypeActive ( entry . key ) "
}
(change)="onSubtypeToggle(entry.key)">
< / mat-form-field >
{{ entry.value | translate }}
< mat-form-field appearance = "outline" class = "tb-iot-hub-sort-field" >
< / mat-checkbox >
< mat-select [ value ] = " selectedSortIndex " ( selectionChange ) = " onSortChange ( $ event . value ) " >
}
@for (option of sortOptions; track option.value; let i = $index) {
< / mat-expansion-panel >
< mat-option [ value ] = " i " > {{ option.label | translate }}< / mat-option >
}
}
< / mat-select >
< / mat-form-field >
< / div >
<!-- Filter dropdowns row -->
@if (categories.size > 0) {
< div class = "tb-iot-hub-filter-row" >
< mat-expansion-panel [ expanded ] = " true " class = "tb-iot-hub-filter-panel" >
@if (getSubtypeMap(); as subtypeMap) {
< mat-expansion-panel-header >
< mat-form-field appearance = "outline" class = "tb-iot-hub-filter-select" >
< mat-panel-title > {{ 'iot-hub.category' | translate }}< / mat-panel-title >
< mat-select [ value ] = " getActiveSubtypesArray ( ) " multiple
< / mat-expansion-panel-header >
[placeholder]="'iot-hub.type-filter-label' | translate"
@for (entry of categories | keyvalue; track entry.key) {
(selectionChange)="onSubtypesChange($event.value)">
< mat-checkbox [ checked ] = " isCategoryActive ( entry . key ) "
@for (entry of subtypeMap | keyvalue; track entry.key) {
(change)="onCategoryToggle(entry.key)">
< mat-option [ value ] = " entry . key " > {{ entry.value | translate }}< / mat-option >
{{ entry.value | translate }}
}
< / mat-checkbox >
< / mat-select >
}
< / mat-form-field >
< / mat-expansion-panel >
}
@if (categories.size > 0) {
< mat-form-field appearance = "outline" class = "tb-iot-hub-filter-select" >
< mat-select [ value ] = " getActiveCategoriesArray ( ) " multiple
[placeholder]="'iot-hub.category' | translate"
(selectionChange)="onCategoriesChange($event.value)">
@for (entry of categories | keyvalue; track entry.key) {
< mat-option [ value ] = " entry . key " > {{ entry.value | translate }}< / mat-option >
}
< / mat-select >
< / mat-form-field >
}
< mat-form-field appearance = "outline" class = "tb-iot-hub-filter-select" >
< mat-select [ value ] = " getActiveUseCasesArray ( ) " multiple
[placeholder]="'iot-hub.use-case' | translate"
(selectionChange)="onUseCasesChange($event.value)">
@for (entry of useCases | keyvalue; track entry.key) {
< mat-option [ value ] = " entry . key " > {{ entry.value | translate }}< / mat-option >
}
}
< / mat-select >
< / mat-form-field >
< / div >
<!-- Active filter chips -->
@if (hasActiveDropdownFilters()) {
< div class = "tb-iot-hub-active-filters" >
@for (key of getActiveSubtypesArray(); track key) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onSubtypeToggle ( key ) " >
{{ getSubtypeLabel(key) }}
< mat-icon > close< / mat-icon >
< / span >
}
@for (key of getActiveCategoriesArray(); track key) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onCategoryToggle ( key ) " >
{{ getCategoryLabel(key) }}
< mat-icon > close< / mat-icon >
< / span >
}
@for (key of getActiveUseCasesArray(); track key) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onUseCaseToggle ( key ) " >
{{ getUseCaseLabel(key) }}
< mat-icon > close< / mat-icon >
< / span >
}
< button class = "tb-iot-hub-clear-filters" ( click ) = " clearAllFilters ( ) " >
{{ 'iot-hub.clear-all-filters' | translate }}
< / button >
< / div >
}
<!-- Results count -->
@if (!isLoading & & !hasError) {
< div class = "tb-iot-hub-results-count" >
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
< / div >
}
<!-- Loading -->
< mat-expansion-panel [ expanded ] = " false " class = "tb-iot-hub-filter-panel" >
@if (isLoading) {
< mat-expansion-panel-header >
< div class = "tb-iot-hub-loading" >
< mat-panel-title > {{ 'iot-hub.use-case' | translate }}< / mat-panel-title >
< mat-spinner diameter = "40" > < / mat-spinner >
< / mat-expansion-panel-header >
@for (entry of useCases | keyvalue; track entry.key) {
< mat-checkbox [ checked ] = " isUseCaseActive ( entry . key ) "
(change)="onUseCaseToggle(entry.key)">
{{ entry.value | translate }}
< / mat-checkbox >
}
< / mat-expansion-panel >
< / div >
< / div >
< / div >
}
<!-- Error -->
<!-- Right content area -->
@if (hasError & & !isLoading) {
< div class = "tb-iot-hub-results" >
< div class = "tb-iot-hub-empty-state" >
<!-- Top bar: results count + search + sort -->
< mat-icon class = "tb-iot-hub-empty-icon" > error_outline< / mat-icon >
< div class = "tb-iot-hub-results-top" >
< h3 > {{ 'iot-hub.unable-to-load' | translate }}< / h3 >
< span class = "tb-iot-hub-results-count" >
< p > {{ 'iot-hub.unable-to-load-text' | translate }}< / p >
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
< button mat-raised-button color = "primary" ( click ) = " loadItems ( ) " >
< / span >
{{ 'iot-hub.retry' | translate }}
< mat-form-field appearance = "outline" class = "tb-iot-hub-search-field" >
< / button >
< mat-icon matPrefix > search< / mat-icon >
< / div >
< input matInput
}
[placeholder]="'iot-hub.search-placeholder' | translate"
[value]="textSearch"
(input)="onSearchChange($any($event.target).value)">
@if (textSearch) {
< button matSuffix mat-icon-button ( click ) = " onSearchChange ( ' ' ) " >
< mat-icon > close< / mat-icon >
< / button >
}
< / mat-form-field >
< / div >
<!-- Empty state -->
<!-- Sort button -->
@if (!isLoading & & !hasError & & items.length === 0) {
< div class = "tb-iot-hub-sort-row" >
< div class = "tb-iot-hub-empty-state" >
<!-- Active filter chips -->
< mat-icon class = "tb-iot-hub-empty-icon" > search_off< / mat-icon >
@if (hasActiveDropdownFilters()) {
< h3 > {{ 'iot-hub.no-items-found' | translate }}< / h3 >
< div class = "tb-iot-hub-active-filters" >
< p > {{ 'iot-hub.no-items-found-text' | translate }}< / p >
@for (key of getActiveSubtypesArray(); track key) {
@if (hasActiveFilters()) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onSubtypeToggle ( key ) " >
< button mat-raised-button color = "primary" ( click ) = " clearAllFilters ( ) " >
{{ getSubtypeLabel(key) }}
{{ 'iot-hub.clear-all-filters' | translate }}
< mat-icon > close< / mat-icon >
< / span >
}
@for (key of getActiveCategoriesArray(); track key) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onCategoryToggle ( key ) " >
{{ getCategoryLabel(key) }}
< mat-icon > close< / mat-icon >
< / span >
}
@for (key of getActiveUseCasesArray(); track key) {
< span class = "tb-iot-hub-active-chip" ( click ) = " onUseCaseToggle ( key ) " >
{{ getUseCaseLabel(key) }}
< mat-icon > close< / mat-icon >
< / span >
}
< button class = "tb-iot-hub-clear-btn" ( click ) = " clearAllFilters ( ) " >
{{ 'iot-hub.clear-all-filters' | translate }}
< / button >
< / div >
}
< span class = "tb-iot-hub-sort-spacer" > < / span >
< button mat-button class = "tb-iot-hub-sort-btn" [ matMenuTriggerFor ] = " sortMenu " >
< mat-icon > filter_list< / mat-icon >
{{ sortOptions[selectedSortIndex].label | translate }}
< / button >
< / 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 >
<!-- Loading -->
@if (isLoading) {
< div class = "tb-iot-hub-loading" >
< mat-spinner diameter = "40" > < / mat-spinner >
< / div >
}
}
< / div >
}
<!-- Card grid -->
<!-- Error -->
@if (!isLoading & & !hasError & & items.length > 0) {
@if (hasError & & !isLoading) {
< div class = "tb-iot-hub-card-grid" >
< div class = "tb-iot-hub-empty-state" >
@for (item of items; track item.id) {
< mat-icon class = "tb-iot-hub-empty-icon" > error_outline< / mat-icon >
< tb-iot-hub-item-card
< h3 > {{ 'iot-hub.unable-to-load' | translate }}< / h3 >
[item]="item"
< p > {{ 'iot-hub.unable-to-load-text' | translate }}< / p >
[installedItem]="getInstalledItem(item)"
< button mat-flat-button color = "primary" ( click ) = " loadItems ( ) " >
[showTypeChip]="false"
{{ 'iot-hub.retry' | translate }}
[showCreator]="!creatorId"
< / button >
(cardClick)="openItemDetail($event)"
< / div >
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)">
< / tb-iot-hub-item-card >
}
}
< / div >
<!-- Pagination -->
<!-- Empty state -->
< mat-paginator
@if (!isLoading & & !hasError & & items.length === 0) {
[length]="totalElements"
< div class = "tb-iot-hub-empty-state" >
[pageSize]="pageSize"
< mat-icon class = "tb-iot-hub-empty-icon" > search_off< / mat-icon >
[pageIndex]="pageIndex"
< h3 > {{ 'iot-hub.no-items-found' | translate }}< / h3 >
[pageSizeOptions]="[12, 24, 48]"
< p > {{ 'iot-hub.no-items-found-text' | translate }}< / p >
(page)="onPageChange($event)">
@if (hasActiveFilters()) {
< / mat-paginator >
< button mat-flat-button color = "primary" ( click ) = " clearAllFilters ( ) " >
}
{{ 'iot-hub.clear-all-filters' | translate }}
< / button >
}
< / div >
}
<!-- Footer -->
<!-- Card grid -->
@if (!creatorId) {
@if (!isLoading & & !hasError & & items.length > 0) {
< div class = "tb-iot-hub-browse-footer" >
< div class = "tb-iot-hub-card-grid" >
< a class = "tb-iot-hub-become-creator" href = "https://iothub.thingsboard.io/signup" target = "_blank" >
@for (item of items; track item.id) {
{{ 'iot-hub.become-a-creator' | translate }}
< tb-iot-hub-item-card
< / a >
[item]="item"
[installedItem]="getInstalledItem(item)"
[showTypeChip]="false"
[showSubtype]="true"
[showCreator]="!creatorId"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)">
< / tb-iot-hub-item-card >
}
< / div >
<!-- Pagination -->
< mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageIndex]="pageIndex"
[pageSizeOptions]="[12, 24, 48]"
(page)="onPageChange($event)">
< / mat-paginator >
}
< / div >
< / div >
}
< / div >
< / div >
< / div >