Browse Source
Propagate IoT Hub marketplace browse page to ThingsBoard, allowing TENANT_ADMIN users to discover and install community widgets, dashboards, rule chains, calculated fields, and device profiles. Backend: - Add iot-hub.base-url config (default: https://iot-hub.thingsboard.io) - Add IotHubController with POST /api/iot-hub/versions/{id}/install - Add IotHubService with per-type install logic (widget, dashboard, etc.) - Add IotHubRestClient for IoT Hub public API communication - Expose iotHubBaseUrl via SystemParams Frontend: - Add IoT Hub browse page with search, filtering, sorting, pagination - Add item detail dialog with readme rendering - Add install dialog calling backend install endpoint - Add creator profile page - Add IotHubApiService with dynamic base URL from NgRx auth state - Add IoT Hub menu entry for TENANT_ADMIN users - Add i18n translations for iot-hub, item, item-data sectionspull/15347/head
31 changed files with 3902 additions and 3 deletions
@ -0,0 +1,48 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import io.swagger.v3.oas.annotations.Hidden; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.ResponseBody; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.iot_hub.IotHubService; |
|||
|
|||
@Hidden |
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api/iot-hub") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class IotHubController extends BaseController { |
|||
|
|||
private final IotHubService iotHubService; |
|||
|
|||
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
|||
@PostMapping("/versions/{versionId}/install") |
|||
@ResponseBody |
|||
public JsonNode installItemVersion(@PathVariable String versionId) throws Exception { |
|||
return iotHubService.installItemVersion(getCurrentUser(), versionId); |
|||
} |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.Dashboard; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.cf.CalculatedField; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChainData; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; |
|||
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; |
|||
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; |
|||
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; |
|||
import org.thingsboard.server.service.rule.TbRuleChainService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.dao.rule.RuleChainService; |
|||
|
|||
@Service |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class DefaultIotHubService implements IotHubService { |
|||
|
|||
private final IotHubRestClient iotHubRestClient; |
|||
private final TbWidgetTypeService tbWidgetTypeService; |
|||
private final TbDashboardService tbDashboardService; |
|||
private final TbCalculatedFieldService tbCalculatedFieldService; |
|||
private final RuleChainService ruleChainService; |
|||
private final TbRuleChainService tbRuleChainService; |
|||
private final TbDeviceProfileService tbDeviceProfileService; |
|||
|
|||
@Override |
|||
public JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception { |
|||
TenantId tenantId = user.getTenantId(); |
|||
log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); |
|||
|
|||
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); |
|||
String itemType = versionInfo.get("type").asText(); |
|||
String itemName = versionInfo.get("name").asText(); |
|||
log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); |
|||
|
|||
byte[] fileData = iotHubRestClient.getVersionFileData(versionId); |
|||
log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); |
|||
|
|||
switch (itemType) { |
|||
case "WIDGET": |
|||
installWidget(user, tenantId, fileData); |
|||
break; |
|||
case "DASHBOARD": |
|||
installDashboard(user, tenantId, fileData); |
|||
break; |
|||
case "CALCULATED_FIELD": |
|||
installCalculatedField(user, tenantId, fileData); |
|||
break; |
|||
case "RULE_CHAIN": |
|||
installRuleChain(tenantId, fileData); |
|||
break; |
|||
case "DEVICE": |
|||
installDeviceProfile(user, tenantId, fileData); |
|||
break; |
|||
default: |
|||
throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); |
|||
} |
|||
|
|||
iotHubRestClient.reportVersionInstalled(versionId); |
|||
log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); |
|||
|
|||
return versionInfo; |
|||
} |
|||
|
|||
private void installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { |
|||
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromBytes(fileData, WidgetTypeDetails.class); |
|||
widgetTypeDetails.setId(null); |
|||
widgetTypeDetails.setTenantId(tenantId); |
|||
tbWidgetTypeService.save(widgetTypeDetails, true, user); |
|||
log.debug("[{}] Widget installed: {}", tenantId, widgetTypeDetails.getName()); |
|||
} |
|||
|
|||
private void installDashboard(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { |
|||
Dashboard dashboard = JacksonUtil.fromBytes(fileData, Dashboard.class); |
|||
dashboard.setId(null); |
|||
dashboard.setTenantId(tenantId); |
|||
tbDashboardService.save(dashboard, user); |
|||
log.debug("[{}] Dashboard installed: {}", tenantId, dashboard.getTitle()); |
|||
} |
|||
|
|||
private void installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { |
|||
CalculatedField calculatedField = JacksonUtil.fromBytes(fileData, CalculatedField.class); |
|||
calculatedField.setId(null); |
|||
calculatedField.setTenantId(tenantId); |
|||
tbCalculatedFieldService.save(calculatedField, user); |
|||
log.debug("[{}] Calculated field installed: {}", tenantId, calculatedField.getName()); |
|||
} |
|||
|
|||
private void installRuleChain(TenantId tenantId, byte[] fileData) { |
|||
RuleChainData ruleChainData = JacksonUtil.fromBytes(fileData, RuleChainData.class); |
|||
ruleChainService.importTenantRuleChains(tenantId, ruleChainData, false, tbRuleChainService::updateRuleNodeConfiguration); |
|||
log.debug("[{}] Rule chain(s) installed", tenantId); |
|||
} |
|||
|
|||
private void installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { |
|||
DeviceProfile deviceProfile = JacksonUtil.fromBytes(fileData, DeviceProfile.class); |
|||
deviceProfile.setId(null); |
|||
deviceProfile.setTenantId(tenantId); |
|||
tbDeviceProfileService.save(deviceProfile, user); |
|||
log.debug("[{}] Device profile installed: {}", tenantId, deviceProfile.getName()); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.client.RestTemplate; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
@Component |
|||
@TbCoreComponent |
|||
@Slf4j |
|||
public class IotHubRestClient { |
|||
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
@Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}") |
|||
private String baseUrl; |
|||
|
|||
public JsonNode getVersionInfo(String versionId) { |
|||
String url = baseUrl + "/api/versions/" + versionId; |
|||
log.debug("Fetching IoT Hub version info: {}", url); |
|||
return restTemplate.getForObject(url, JsonNode.class); |
|||
} |
|||
|
|||
public byte[] getVersionFileData(String versionId) { |
|||
String url = baseUrl + "/api/versions/" + versionId + "/fileData"; |
|||
log.debug("Fetching IoT Hub version file data: {}", url); |
|||
ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class); |
|||
return response.getBody(); |
|||
} |
|||
|
|||
public void reportVersionInstalled(String versionId) { |
|||
String url = baseUrl + "/api/versions/" + versionId + "/install"; |
|||
log.debug("Reporting IoT Hub version installed: {}", url); |
|||
restTemplate.postForObject(url, null, Void.class); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
|
|||
public interface IotHubService { |
|||
|
|||
JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception; |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
///
|
|||
/// 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 { Injectable } from '@angular/core'; |
|||
import { HttpClient, HttpParams } from '@angular/common/http'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { Observable } from 'rxjs'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; |
|||
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; |
|||
import { InterceptorConfig } from '@core/interceptors/interceptor-config'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { getCurrentAuthState } from '@core/auth/auth.selectors'; |
|||
import { environment as env } from '@env/environment'; |
|||
|
|||
export function tbVersionToInt(version: string): number { |
|||
const parts = version.replace(/[^0-9.]/g, '').split('.'); |
|||
const major = parseInt(parts[0], 10) || 0; |
|||
const minor = parseInt(parts[1], 10) || 0; |
|||
const patch = parseInt(parts[2], 10) || 0; |
|||
return major * 100 + minor * 10 + patch; |
|||
} |
|||
|
|||
export function iotHubResourceUrl(baseUrl: string, path: string): string { |
|||
if (!path) { |
|||
return path; |
|||
} |
|||
if (path.startsWith('http://') || path.startsWith('https://')) { |
|||
return path; |
|||
} |
|||
return `${baseUrl}${path.startsWith('/') ? '' : '/'}${path}`; |
|||
} |
|||
|
|||
export interface IotHubRequestConfig { |
|||
ignoreLoading?: boolean; |
|||
ignoreErrors?: boolean; |
|||
} |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class IotHubApiService { |
|||
|
|||
constructor( |
|||
private http: HttpClient, |
|||
private store: Store<AppState> |
|||
) {} |
|||
|
|||
get baseUrl(): string { |
|||
return getCurrentAuthState(this.store)?.iotHubBaseUrl || ''; |
|||
} |
|||
|
|||
public resolveResourceUrl(path: string): string { |
|||
return iotHubResourceUrl(this.baseUrl, path); |
|||
} |
|||
|
|||
public getPublishedVersions(query: MpItemVersionQuery, config?: IotHubRequestConfig): Observable<PageData<MpItemVersionView>> { |
|||
if (query.tbVersion == null) { |
|||
query.tbVersion = tbVersionToInt(env.tbVersion); |
|||
} |
|||
if (query.peOnly == null) { |
|||
query.peOnly = false; |
|||
} |
|||
return this.http.get<PageData<MpItemVersionView>>( |
|||
`${this.baseUrl}/api/versions/published${query.toQuery()}`, |
|||
{ params: this.buildParams(config) } |
|||
); |
|||
} |
|||
|
|||
public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable<string> { |
|||
return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { |
|||
params: this.buildParams(config), |
|||
responseType: 'text' |
|||
}); |
|||
} |
|||
|
|||
public getVersionFileData(versionId: string, config?: IotHubRequestConfig): Observable<Blob> { |
|||
return this.http.get(`${this.baseUrl}/api/versions/${versionId}/fileData`, { |
|||
params: this.buildParams(config), |
|||
responseType: 'blob' |
|||
}); |
|||
} |
|||
|
|||
public reportVersionInstalled(versionId: string, config?: IotHubRequestConfig): Observable<void> { |
|||
return this.http.post<void>( |
|||
`${this.baseUrl}/api/versions/${versionId}/install`, |
|||
null, |
|||
{ params: this.buildParams(config) } |
|||
); |
|||
} |
|||
|
|||
public installItemVersion(versionId: string, config?: IotHubRequestConfig): Observable<any> { |
|||
return this.http.post<any>( |
|||
`/api/iot-hub/versions/${versionId}/install`, |
|||
null, |
|||
{ params: this.buildParams(config) } |
|||
); |
|||
} |
|||
|
|||
public getCreatorProfile(creatorId: string, config?: IotHubRequestConfig): Observable<CreatorView> { |
|||
return this.http.get<CreatorView>( |
|||
`${this.baseUrl}/api/creators/${creatorId}/profile`, |
|||
{ params: this.buildParams(config) } |
|||
); |
|||
} |
|||
|
|||
private buildParams(config?: IotHubRequestConfig): HttpParams { |
|||
return new InterceptorHttpParams( |
|||
new InterceptorConfig(config?.ignoreLoading ?? false, config?.ignoreErrors ?? false) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
<!-- |
|||
|
|||
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-browse-container" [class.embedded]="embedded"> |
|||
<!-- Page header --> |
|||
@if (!embedded) { |
|||
<div class="tb-iot-hub-browse-header"> |
|||
<h1>{{ getTitle() | translate }}</h1> |
|||
<p>{{ getSubtitle() | translate }}</p> |
|||
</div> |
|||
} |
|||
|
|||
<!-- Type tabs --> |
|||
<nav mat-tab-nav-bar class="tb-iot-hub-type-tabs" [tabPanel]="tabPanel"> |
|||
<a mat-tab-link |
|||
[active]="activeType === ItemType.WIDGET" |
|||
(click)="onTypeTabChange(ItemType.WIDGET)"> |
|||
{{ 'item.type-widget' | translate }}s |
|||
</a> |
|||
<a mat-tab-link |
|||
[active]="activeType === ItemType.DASHBOARD" |
|||
(click)="onTypeTabChange(ItemType.DASHBOARD)"> |
|||
{{ 'item.type-dashboard' | translate }}s |
|||
</a> |
|||
<a mat-tab-link |
|||
[active]="activeType === ItemType.CALCULATED_FIELD" |
|||
(click)="onTypeTabChange(ItemType.CALCULATED_FIELD)"> |
|||
{{ 'item.type-calculated-field' | translate }}s |
|||
</a> |
|||
<a mat-tab-link |
|||
[active]="activeType === ItemType.RULE_CHAIN" |
|||
(click)="onTypeTabChange(ItemType.RULE_CHAIN)"> |
|||
{{ 'item.type-rule-chain' | translate }}s |
|||
</a> |
|||
<a mat-tab-link |
|||
[active]="activeType === ItemType.DEVICE" |
|||
(click)="onTypeTabChange(ItemType.DEVICE)"> |
|||
{{ 'item.type-device' | translate }}s |
|||
</a> |
|||
</nav> |
|||
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> |
|||
|
|||
<!-- Search + sort row --> |
|||
<div class="tb-iot-hub-search-row"> |
|||
<mat-form-field appearance="outline" class="tb-iot-hub-search-field"> |
|||
<mat-icon matPrefix>search</mat-icon> |
|||
<input matInput |
|||
[placeholder]="getSearchPlaceholder() | 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> |
|||
<mat-form-field appearance="outline" class="tb-iot-hub-sort-field"> |
|||
<mat-select [value]="selectedSortIndex" (selectionChange)="onSortChange($event.value)"> |
|||
@for (option of sortOptions; track option.value; let i = $index) { |
|||
<mat-option [value]="i">{{ option.label | translate }}</mat-option> |
|||
} |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
|
|||
<!-- Filter dropdowns row --> |
|||
<div class="tb-iot-hub-filter-row"> |
|||
@if (getSubtypeMap(); as subtypeMap) { |
|||
<mat-form-field appearance="outline" class="tb-iot-hub-filter-select"> |
|||
<mat-select [value]="getActiveSubtypesArray()" multiple |
|||
[placeholder]="'iot-hub.type-filter-label' | translate" |
|||
(selectionChange)="onSubtypesChange($event.value)"> |
|||
@for (entry of subtypeMap | keyvalue; track entry.key) { |
|||
<mat-option [value]="entry.key">{{ entry.value | translate }}</mat-option> |
|||
} |
|||
</mat-select> |
|||
</mat-form-field> |
|||
} |
|||
@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 --> |
|||
@if (isLoading) { |
|||
<div class="tb-iot-hub-loading"> |
|||
<mat-spinner diameter="40"></mat-spinner> |
|||
</div> |
|||
} |
|||
|
|||
<!-- Error --> |
|||
@if (hasError && !isLoading) { |
|||
<div class="tb-iot-hub-empty-state"> |
|||
<mat-icon class="tb-iot-hub-empty-icon">error_outline</mat-icon> |
|||
<h3>{{ 'iot-hub.unable-to-load' | translate }}</h3> |
|||
<p>{{ 'iot-hub.unable-to-load-text' | translate }}</p> |
|||
<button mat-raised-button color="primary" (click)="loadItems()"> |
|||
{{ 'iot-hub.retry' | translate }} |
|||
</button> |
|||
</div> |
|||
} |
|||
|
|||
<!-- Empty state --> |
|||
@if (!isLoading && !hasError && items.length === 0) { |
|||
<div class="tb-iot-hub-empty-state"> |
|||
<mat-icon class="tb-iot-hub-empty-icon">search_off</mat-icon> |
|||
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3> |
|||
<p>{{ 'iot-hub.no-items-found-text' | translate }}</p> |
|||
@if (hasActiveFilters()) { |
|||
<button mat-raised-button color="primary" (click)="clearAllFilters()"> |
|||
{{ 'iot-hub.clear-all-filters' | translate }} |
|||
</button> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- Card grid --> |
|||
@if (!isLoading && !hasError && items.length > 0) { |
|||
<div class="tb-iot-hub-card-grid"> |
|||
@for (item of items; track item.id) { |
|||
<tb-iot-hub-item-card |
|||
[item]="item" |
|||
[showTypeChip]="false" |
|||
[showCreator]="!creatorId" |
|||
(cardClick)="openItemDetail($event)" |
|||
(creatorClick)="navigateToCreator($event)" |
|||
(installClick)="installItem($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> |
|||
} |
|||
|
|||
<!-- Footer --> |
|||
@if (!creatorId) { |
|||
<div class="tb-iot-hub-browse-footer"> |
|||
<a class="tb-iot-hub-become-creator" href="https://iothub.thingsboard.io/signup" target="_blank"> |
|||
{{ 'iot-hub.become-a-creator' | translate }} |
|||
</a> |
|||
</div> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,400 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
:host { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
padding: 16px 24px; |
|||
overflow: auto; |
|||
|
|||
&.embedded { |
|||
padding: 0; |
|||
overflow: visible; |
|||
height: auto; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-browse-container { |
|||
max-width: 1400px; |
|||
margin: 0 auto; |
|||
width: 100%; |
|||
background: white; |
|||
padding: 24px; |
|||
border-radius: 4px; |
|||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12); |
|||
|
|||
&.embedded { |
|||
max-width: none; |
|||
padding: 0; |
|||
border-radius: 0; |
|||
box-shadow: none; |
|||
background: transparent; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-browse-header { |
|||
margin-bottom: 24px; |
|||
|
|||
h1 { |
|||
font-size: 28px; |
|||
font-weight: 700; |
|||
margin: 0 0 4px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
|
|||
p { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-type-tabs { |
|||
margin-bottom: 16px; |
|||
border-bottom: 1px solid rgba(0, 0, 0, 0.12); |
|||
|
|||
::ng-deep { |
|||
.mat-mdc-tab-link-container { |
|||
flex-grow: 0; |
|||
} |
|||
|
|||
.mat-mdc-tab-link { |
|||
text-transform: none; |
|||
font-weight: 400; |
|||
min-width: auto; |
|||
padding: 0 16px; |
|||
letter-spacing: normal; |
|||
height: 40px; |
|||
flex: 0 0 auto; |
|||
} |
|||
|
|||
--mat-tab-active-indicator-color: #1976d2; |
|||
|
|||
.mdc-tab__text-label { |
|||
color: rgba(0, 0, 0, 0.6); |
|||
} |
|||
|
|||
.mdc-tab--active .mdc-tab__text-label { |
|||
font-weight: 500; |
|||
} |
|||
|
|||
--mat-tab-active-label-text-color: #1976d2; |
|||
--mat-tab-active-focus-label-text-color: #1976d2; |
|||
--mat-tab-active-hover-label-text-color: #1976d2; |
|||
--mat-tab-active-ripple-color: #1976d2; |
|||
--mat-tab-inactive-ripple-color: #1976d2; |
|||
--mat-tab-active-focus-indicator-color: #1976d2; |
|||
} |
|||
} |
|||
|
|||
// Shared field styles for search/filter/sort dropdowns |
|||
%tb-iot-hub-field-base { |
|||
::ng-deep { |
|||
.mat-mdc-text-field-wrapper { |
|||
height: 42px; |
|||
padding: 0 12px; |
|||
} |
|||
|
|||
.mat-mdc-form-field-flex { |
|||
height: 42px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.mdc-notched-outline__leading, |
|||
.mdc-notched-outline__notch, |
|||
.mdc-notched-outline__trailing { |
|||
border-color: #d5dbe3; |
|||
} |
|||
|
|||
.mdc-notched-outline__leading { |
|||
border-radius: 8px 0 0 8px; |
|||
} |
|||
|
|||
.mdc-notched-outline__trailing { |
|||
border-radius: 0 8px 8px 0; |
|||
} |
|||
|
|||
.mat-mdc-form-field-infix { |
|||
padding-top: 0; |
|||
padding-bottom: 0; |
|||
min-height: unset; |
|||
} |
|||
|
|||
.mdc-notched-outline__notch { |
|||
display: none; |
|||
} |
|||
|
|||
.mat-mdc-floating-label { |
|||
display: none; |
|||
} |
|||
|
|||
.mat-mdc-form-field-subscript-wrapper { |
|||
display: none; |
|||
} |
|||
|
|||
input.mat-mdc-input-element { |
|||
font-size: 14px; |
|||
color: #3e4a59; |
|||
|
|||
&::placeholder { |
|||
color: #8b95a2; |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-select-value-text { |
|||
font-size: 14px; |
|||
color: #3e4a59; |
|||
} |
|||
|
|||
.mat-mdc-select-arrow-wrapper { |
|||
padding-left: 4px; |
|||
} |
|||
|
|||
.mat-icon { |
|||
color: #8b95a2; |
|||
font-size: 20px; |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
} |
|||
|
|||
&.mat-focused ::ng-deep { |
|||
.mdc-notched-outline__leading, |
|||
.mdc-notched-outline__notch, |
|||
.mdc-notched-outline__trailing { |
|||
border-color: #8b95a2; |
|||
border-width: 1px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-search-row { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-bottom: 12px; |
|||
align-items: center; |
|||
|
|||
.tb-iot-hub-search-field, |
|||
.tb-iot-hub-sort-field { |
|||
@extend %tb-iot-hub-field-base; |
|||
} |
|||
|
|||
.tb-iot-hub-search-field { |
|||
flex: 1; |
|||
|
|||
::ng-deep { |
|||
.mat-mdc-form-field-icon-prefix { |
|||
padding: 0 8px 0 0; |
|||
|
|||
> .mat-icon { |
|||
padding: 0; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-form-field-icon-suffix { |
|||
padding: 0; |
|||
|
|||
.mat-mdc-icon-button { |
|||
width: 32px; |
|||
height: 32px; |
|||
padding: 4px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-sort-field { |
|||
width: 140px; |
|||
flex-shrink: 0; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-filter-row { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-bottom: 16px; |
|||
padding-right: 152px; |
|||
|
|||
.tb-iot-hub-filter-select { |
|||
@extend %tb-iot-hub-field-base; |
|||
flex: 1; |
|||
min-width: 0; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-active-filters { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
flex-wrap: wrap; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.tb-iot-hub-active-chip { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
padding: 4px 8px 4px 12px; |
|||
border-radius: 16px; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
background: #eef4ff; |
|||
color: #305ECA; |
|||
cursor: pointer; |
|||
white-space: nowrap; |
|||
transition: background-color 0.15s; |
|||
|
|||
mat-icon { |
|||
font-size: 14px; |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
|
|||
&:hover { |
|||
background: #dde8fc; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-clear-filters { |
|||
border: none; |
|||
background: none; |
|||
font-size: 12px; |
|||
color: #8b95a2; |
|||
cursor: pointer; |
|||
padding: 4px 8px; |
|||
white-space: nowrap; |
|||
|
|||
&:hover { |
|||
color: #3e4a59; |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-results-count { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.tb-iot-hub-loading { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
min-height: 200px; |
|||
} |
|||
|
|||
.tb-iot-hub-empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 300px; |
|||
text-align: center; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
|
|||
.tb-iot-hub-empty-icon { |
|||
font-size: 64px; |
|||
width: 64px; |
|||
height: 64px; |
|||
margin-bottom: 16px; |
|||
opacity: 0.4; |
|||
} |
|||
|
|||
h3 { |
|||
margin: 0 0 8px 0; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
|
|||
p { |
|||
margin: 0 0 16px 0; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-card-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|||
gap: 16px; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
::ng-deep mat-paginator { |
|||
.mat-mdc-form-field { |
|||
.mat-mdc-text-field-wrapper { |
|||
height: 42px; |
|||
padding: 0 12px; |
|||
border: 1px solid #d5dbe3; |
|||
border-radius: 8px; |
|||
background: white; |
|||
} |
|||
|
|||
.mat-mdc-form-field-flex { |
|||
height: 42px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.mdc-notched-outline { |
|||
display: none; |
|||
} |
|||
|
|||
.mat-mdc-form-field-infix { |
|||
padding-top: 0; |
|||
padding-bottom: 0; |
|||
min-height: unset; |
|||
border-top: none; |
|||
} |
|||
|
|||
.mat-mdc-floating-label { |
|||
display: none; |
|||
} |
|||
|
|||
.mat-mdc-form-field-subscript-wrapper { |
|||
display: none; |
|||
} |
|||
|
|||
.mat-mdc-select-value-text { |
|||
font-size: 14px; |
|||
color: #3e4a59; |
|||
} |
|||
|
|||
.mat-mdc-select-arrow-wrapper { |
|||
padding-left: 4px; |
|||
} |
|||
|
|||
&.mat-focused .mat-mdc-text-field-wrapper { |
|||
border-color: #8b95a2; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-browse-footer { |
|||
text-align: center; |
|||
padding: 24px 0; |
|||
border-top: 1px solid rgba(0, 0, 0, 0.08); |
|||
margin-top: 16px; |
|||
|
|||
.tb-iot-hub-become-creator { |
|||
color: rgba(0, 0, 0, 0.38); |
|||
font-size: 13px; |
|||
text-decoration: none; |
|||
|
|||
&:hover { |
|||
color: rgba(0, 0, 0, 0.54); |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,388 @@ |
|||
///
|
|||
/// 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, Input } from '@angular/core'; |
|||
import { Subject } from 'rxjs'; |
|||
import { debounceTime, takeUntil } from 'rxjs/operators'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { |
|||
ItemType, |
|||
getCategoriesForType, useCaseTranslations |
|||
} from '@shared/models/iot-hub/iot-hub-item.models'; |
|||
import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; |
|||
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; |
|||
|
|||
interface SortOption { |
|||
value: string; |
|||
label: string; |
|||
direction: Direction; |
|||
} |
|||
|
|||
const TYPE_TABS: ItemType[] = [ |
|||
ItemType.WIDGET, |
|||
ItemType.DASHBOARD, |
|||
ItemType.CALCULATED_FIELD, |
|||
ItemType.RULE_CHAIN, |
|||
ItemType.DEVICE |
|||
]; |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-browse', |
|||
standalone: false, |
|||
templateUrl: './iot-hub-browse.component.html', |
|||
styleUrls: ['./iot-hub-browse.component.scss'], |
|||
host: { '[class.embedded]': 'embedded' } |
|||
}) |
|||
export class TbIotHubBrowseComponent implements OnInit, OnDestroy { |
|||
|
|||
readonly ItemType = ItemType; |
|||
|
|||
@Input() creatorId: string; |
|||
@Input() embedded = false; |
|||
|
|||
items: MpItemVersionView[] = []; |
|||
totalElements = 0; |
|||
pageSize = 12; |
|||
pageIndex = 0; |
|||
isLoading = false; |
|||
hasError = false; |
|||
|
|||
textSearch = ''; |
|||
activeType: ItemType = ItemType.WIDGET; |
|||
activeCategories = new Set<string>(); |
|||
activeUseCases = new Set<string>(); |
|||
activeCfTypes = new Set<string>(); |
|||
activeWidgetTypes = new Set<string>(); |
|||
activeRuleChainTypes = new Set<string>(); |
|||
|
|||
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; |
|||
|
|||
categories = new Map<string, string>(); |
|||
useCases: Map<string, string> = useCaseTranslations as Map<string, string>; |
|||
cfTypes: Map<string, string> = cfTypeTranslations; |
|||
widgetTypes: Map<string, string> = widgetTypeTranslations; |
|||
ruleChainTypes: Map<string, string> = ruleChainTypeTranslations; |
|||
|
|||
private searchSubject = new Subject<string>(); |
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor( |
|||
private iotHubApiService: IotHubApiService, |
|||
private dialog: MatDialog, |
|||
private translate: TranslateService, |
|||
private router: Router |
|||
) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.searchSubject.pipe( |
|||
debounceTime(300), |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => { |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
}); |
|||
this.updateCategories(); |
|||
this.loadItems(); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
onSearchChange(value: string): void { |
|||
this.textSearch = value; |
|||
this.searchSubject.next(value); |
|||
} |
|||
|
|||
getTypeTabIndex(): number { |
|||
return TYPE_TABS.indexOf(this.activeType); |
|||
} |
|||
|
|||
onTypeTabIndexChange(index: number): void { |
|||
this.onTypeTabChange(TYPE_TABS[index]); |
|||
} |
|||
|
|||
onTypeTabChange(type: ItemType): void { |
|||
this.activeType = type; |
|||
this.activeCategories.clear(); |
|||
this.activeUseCases.clear(); |
|||
this.activeCfTypes.clear(); |
|||
this.activeWidgetTypes.clear(); |
|||
this.activeRuleChainTypes.clear(); |
|||
this.updateCategories(); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onCategoryToggle(category: string): void { |
|||
if (this.activeCategories.has(category)) { |
|||
this.activeCategories.delete(category); |
|||
} else { |
|||
this.activeCategories.add(category); |
|||
} |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onSortChange(index: number): void { |
|||
this.selectedSortIndex = index; |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onPageChange(event: { pageIndex: number; pageSize: number }): void { |
|||
this.pageIndex = event.pageIndex; |
|||
this.pageSize = event.pageSize; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
clearCategories(): void { |
|||
this.activeCategories.clear(); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onUseCaseToggle(useCase: string): void { |
|||
if (this.activeUseCases.has(useCase)) { |
|||
this.activeUseCases.delete(useCase); |
|||
} else { |
|||
this.activeUseCases.add(useCase); |
|||
} |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
clearUseCases(): void { |
|||
this.activeUseCases.clear(); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
getSubtypeMap(): Map<string, string> | null { |
|||
switch (this.activeType) { |
|||
case ItemType.WIDGET: return this.widgetTypes; |
|||
case ItemType.CALCULATED_FIELD: return this.cfTypes; |
|||
case ItemType.RULE_CHAIN: return this.ruleChainTypes; |
|||
default: return null; |
|||
} |
|||
} |
|||
|
|||
getActiveSubtypes(): Set<string> { |
|||
switch (this.activeType) { |
|||
case ItemType.WIDGET: return this.activeWidgetTypes; |
|||
case ItemType.CALCULATED_FIELD: return this.activeCfTypes; |
|||
case ItemType.RULE_CHAIN: return this.activeRuleChainTypes; |
|||
default: return new Set(); |
|||
} |
|||
} |
|||
|
|||
onSubtypeToggle(subtype: string): void { |
|||
const active = this.getActiveSubtypes(); |
|||
if (active.has(subtype)) { |
|||
active.delete(subtype); |
|||
} else { |
|||
active.add(subtype); |
|||
} |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
clearSubtypes(): void { |
|||
this.getActiveSubtypes().clear(); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
getActiveSubtypesArray(): string[] { |
|||
return Array.from(this.getActiveSubtypes()); |
|||
} |
|||
|
|||
getActiveCategoriesArray(): string[] { |
|||
return Array.from(this.activeCategories); |
|||
} |
|||
|
|||
getActiveUseCasesArray(): string[] { |
|||
return Array.from(this.activeUseCases); |
|||
} |
|||
|
|||
onSubtypesChange(values: string[]): void { |
|||
const active = this.getActiveSubtypes(); |
|||
active.clear(); |
|||
values.forEach(v => active.add(v)); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onCategoriesChange(values: string[]): void { |
|||
this.activeCategories.clear(); |
|||
values.forEach(v => this.activeCategories.add(v)); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
onUseCasesChange(values: string[]): void { |
|||
this.activeUseCases.clear(); |
|||
values.forEach(v => this.activeUseCases.add(v)); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
getSubtypeLabel(key: string): string { |
|||
const map = this.getSubtypeMap(); |
|||
const translationKey = map?.get(key); |
|||
return translationKey ? this.translate.instant(translationKey) : key; |
|||
} |
|||
|
|||
getCategoryLabel(key: string): string { |
|||
const translationKey = this.categories.get(key); |
|||
return translationKey ? this.translate.instant(translationKey) : key; |
|||
} |
|||
|
|||
getUseCaseLabel(key: string): string { |
|||
const translationKey = this.useCases.get(key); |
|||
return translationKey ? this.translate.instant(translationKey) : key; |
|||
} |
|||
|
|||
clearAllFilters(): void { |
|||
this.activeCategories.clear(); |
|||
this.activeUseCases.clear(); |
|||
this.activeCfTypes.clear(); |
|||
this.activeWidgetTypes.clear(); |
|||
this.activeRuleChainTypes.clear(); |
|||
this.textSearch = ''; |
|||
this.updateCategories(); |
|||
this.pageIndex = 0; |
|||
this.loadItems(); |
|||
} |
|||
|
|||
hasActiveDropdownFilters(): boolean { |
|||
return this.activeCategories.size > 0 || |
|||
this.activeUseCases.size > 0 || this.activeCfTypes.size > 0 || |
|||
this.activeWidgetTypes.size > 0 || this.activeRuleChainTypes.size > 0; |
|||
} |
|||
|
|||
hasActiveFilters(): boolean { |
|||
return this.hasActiveDropdownFilters() || this.textSearch.length > 0; |
|||
} |
|||
|
|||
getTitle(): string { |
|||
switch (this.activeType) { |
|||
case ItemType.WIDGET: return 'iot-hub.title-widgets'; |
|||
case ItemType.DASHBOARD: return 'iot-hub.title-dashboards'; |
|||
case ItemType.CALCULATED_FIELD: return 'iot-hub.title-calculated-fields'; |
|||
case ItemType.RULE_CHAIN: return 'iot-hub.title-rule-chains'; |
|||
case ItemType.DEVICE: return 'iot-hub.title-devices'; |
|||
} |
|||
} |
|||
|
|||
getSubtitle(): string { |
|||
switch (this.activeType) { |
|||
case ItemType.WIDGET: return 'iot-hub.subtitle-widgets'; |
|||
case ItemType.DASHBOARD: return 'iot-hub.subtitle-dashboards'; |
|||
case ItemType.CALCULATED_FIELD: return 'iot-hub.subtitle-calculated-fields'; |
|||
case ItemType.RULE_CHAIN: return 'iot-hub.subtitle-rule-chains'; |
|||
case ItemType.DEVICE: return 'iot-hub.subtitle-devices'; |
|||
} |
|||
} |
|||
|
|||
getSearchPlaceholder(): string { |
|||
switch (this.activeType) { |
|||
case ItemType.WIDGET: return 'iot-hub.search-widgets'; |
|||
case ItemType.DASHBOARD: return 'iot-hub.search-dashboards'; |
|||
case ItemType.CALCULATED_FIELD: return 'iot-hub.search-calculated-fields'; |
|||
case ItemType.RULE_CHAIN: return 'iot-hub.search-rule-chains'; |
|||
case ItemType.DEVICE: return 'iot-hub.search-devices'; |
|||
} |
|||
} |
|||
|
|||
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]); |
|||
} |
|||
|
|||
private updateCategories(): void { |
|||
this.categories = getCategoriesForType(this.activeType); |
|||
} |
|||
|
|||
loadItems(): void { |
|||
this.isLoading = true; |
|||
this.hasError = false; |
|||
const sort = this.sortOptions[this.selectedSortIndex]; |
|||
const sortOrder: SortOrder = { property: sort.value, direction: sort.direction }; |
|||
const pageLink = new PageLink(this.pageSize, this.pageIndex, this.textSearch || null, sortOrder); |
|||
const query = new MpItemVersionQuery( |
|||
pageLink, |
|||
this.activeType, |
|||
undefined, |
|||
this.creatorId || undefined, |
|||
this.activeCategories.size > 0 ? Array.from(this.activeCategories) : undefined, |
|||
this.activeUseCases.size > 0 ? Array.from(this.activeUseCases) : undefined, |
|||
this.activeCfTypes.size > 0 ? Array.from(this.activeCfTypes) : undefined, |
|||
this.activeWidgetTypes.size > 0 ? Array.from(this.activeWidgetTypes) : undefined, |
|||
this.activeRuleChainTypes.size > 0 ? Array.from(this.activeRuleChainTypes) : undefined |
|||
); |
|||
this.iotHubApiService.getPublishedVersions( |
|||
query, |
|||
{ ignoreLoading: true, ignoreErrors: true } |
|||
).subscribe({ |
|||
next: (data: PageData<MpItemVersionView>) => { |
|||
this.items = data.data; |
|||
this.totalElements = data.totalElements; |
|||
this.isLoading = false; |
|||
}, |
|||
error: () => { |
|||
this.isLoading = false; |
|||
this.hasError = true; |
|||
this.items = []; |
|||
this.totalElements = 0; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
<!-- |
|||
|
|||
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-creator-profile-container"> |
|||
<button mat-button class="tb-iot-hub-back-btn" (click)="goBack()"> |
|||
<mat-icon>arrow_back</mat-icon> |
|||
{{ 'iot-hub.back-to-iot-hub' | translate }} |
|||
</button> |
|||
|
|||
@if (creator) { |
|||
<!-- Creator header --> |
|||
<mat-card class="tb-iot-hub-creator-header" appearance="outlined"> |
|||
<mat-card-content> |
|||
<div class="tb-iot-hub-creator-info"> |
|||
<div class="tb-iot-hub-creator-avatar"> |
|||
@if (getAvatarUrl(); as avatarUrl) { |
|||
<img [src]="avatarUrl" alt=""> |
|||
} @else { |
|||
<mat-icon>account_circle</mat-icon> |
|||
} |
|||
</div> |
|||
<div class="tb-iot-hub-creator-details"> |
|||
<h2>{{ creator.displayName }}</h2> |
|||
@if (creator.description) { |
|||
<p class="tb-iot-hub-creator-desc">{{ creator.description }}</p> |
|||
} |
|||
@if (creator.website) { |
|||
<a class="tb-iot-hub-creator-link" [href]="creator.website" target="_blank"> |
|||
{{ creator.website }} |
|||
</a> |
|||
} |
|||
@if (creator.contactEmail) { |
|||
<div class="tb-iot-hub-creator-email">{{ creator.contactEmail }}</div> |
|||
} |
|||
<div class="tb-iot-hub-social-links"> |
|||
@if (creator.githubUrl) { |
|||
<a [href]="creator.githubUrl" target="_blank" mat-icon-button matTooltip="GitHub"> |
|||
<mat-icon>code</mat-icon> |
|||
</a> |
|||
} |
|||
@if (creator.linkedinUrl) { |
|||
<a [href]="creator.linkedinUrl" target="_blank" mat-icon-button matTooltip="LinkedIn"> |
|||
<mat-icon>work</mat-icon> |
|||
</a> |
|||
} |
|||
@if (creator.twitterUrl) { |
|||
<a [href]="creator.twitterUrl" target="_blank" mat-icon-button matTooltip="Twitter/X"> |
|||
<mat-icon>chat</mat-icon> |
|||
</a> |
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
|
|||
<!-- Published items --> |
|||
<div class="tb-iot-hub-creator-items-header"> |
|||
<h3>{{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }})</h3> |
|||
</div> |
|||
|
|||
<tb-iot-hub-browse |
|||
#browseComponent |
|||
[creatorId]="creatorId" |
|||
[embedded]="true"> |
|||
</tb-iot-hub-browse> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,119 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
:host { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
padding: 16px 24px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.tb-iot-hub-creator-profile-container { |
|||
max-width: 1200px; |
|||
margin: 0 auto; |
|||
width: 100%; |
|||
background: white; |
|||
padding: 24px; |
|||
border-radius: 4px; |
|||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12); |
|||
} |
|||
|
|||
.tb-iot-hub-back-btn { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.tb-iot-hub-creator-header { |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.tb-iot-hub-creator-info { |
|||
display: flex; |
|||
gap: 24px; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.tb-iot-hub-creator-avatar { |
|||
width: 80px; |
|||
height: 80px; |
|||
min-width: 80px; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: rgba(0, 0, 0, 0.06); |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
} |
|||
|
|||
mat-icon { |
|||
font-size: 64px; |
|||
width: 64px; |
|||
height: 64px; |
|||
color: rgba(0, 0, 0, 0.3); |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-creator-details { |
|||
flex: 1; |
|||
|
|||
h2 { |
|||
margin: 0 0 4px 0; |
|||
font-size: 22px; |
|||
} |
|||
|
|||
.tb-iot-hub-creator-desc { |
|||
margin: 0 0 8px 0; |
|||
color: rgba(0, 0, 0, 0.6); |
|||
} |
|||
|
|||
.tb-iot-hub-creator-link { |
|||
color: #1976d2; |
|||
text-decoration: none; |
|||
font-size: 14px; |
|||
display: block; |
|||
margin-bottom: 4px; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-creator-email { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-social-links { |
|||
display: flex; |
|||
gap: 4px; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-creator-items-header { |
|||
margin-bottom: 16px; |
|||
|
|||
h3 { |
|||
margin: 0; |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
///
|
|||
/// 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 { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; |
|||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-creator-profile', |
|||
standalone: false, |
|||
templateUrl: './iot-hub-creator-profile.component.html', |
|||
styleUrls: ['./iot-hub-creator-profile.component.scss'] |
|||
}) |
|||
export class TbIotHubCreatorProfileComponent implements OnInit, OnDestroy { |
|||
|
|||
creator: CreatorView; |
|||
creatorId: string; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor( |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
private iotHubApiService: IotHubApiService |
|||
) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe(params => { |
|||
this.creatorId = params['creatorId']; |
|||
this.loadCreator(); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
getAvatarUrl(): string | null { |
|||
return this.creator?.avatarUrl ? this.iotHubApiService.resolveResourceUrl(this.creator.avatarUrl) : null; |
|||
} |
|||
|
|||
goBack(): void { |
|||
this.router.navigate(['/iot-hub']); |
|||
} |
|||
|
|||
private loadCreator(): void { |
|||
this.iotHubApiService.getCreatorProfile(this.creatorId, { ignoreLoading: true }).subscribe({ |
|||
next: creator => this.creator = creator, |
|||
error: () => this.router.navigate(['/iot-hub']) |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
///
|
|||
/// 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, Inject } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
|||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
export interface IotHubInstallDialogData { |
|||
item: MpItemVersionView; |
|||
iotHubApiService: IotHubApiService; |
|||
} |
|||
|
|||
export type InstallState = 'confirm' | 'installing' | 'success' | 'error'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-install-dialog', |
|||
standalone: false, |
|||
template: ` |
|||
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2> |
|||
<mat-dialog-content> |
|||
<p>{{ 'iot-hub.install-confirm' | translate:{ name: item.name, version: item.version } }}</p> |
|||
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: (typeTranslations.get(item.type) | translate) } }}</p> |
|||
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-creator' | translate:{ creator: item.creatorDisplayName } }}</p> |
|||
</mat-dialog-content> |
|||
<mat-dialog-actions align="end"> |
|||
<button mat-button (click)="cancel()" [disabled]="state === 'installing'"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
@switch (state) { |
|||
@case ('confirm') { |
|||
<button mat-raised-button color="primary" (click)="install()"> |
|||
{{ 'iot-hub.install' | translate }} |
|||
</button> |
|||
} |
|||
@case ('installing') { |
|||
<button mat-raised-button color="primary" disabled> |
|||
<mat-spinner diameter="18" class="tb-iot-hub-inline-spinner"></mat-spinner> |
|||
{{ 'iot-hub.installing' | translate }} |
|||
</button> |
|||
} |
|||
@case ('success') { |
|||
<button mat-raised-button color="primary" disabled> |
|||
<mat-icon>check</mat-icon> |
|||
{{ 'iot-hub.installed' | translate }} |
|||
</button> |
|||
} |
|||
} |
|||
</mat-dialog-actions> |
|||
`,
|
|||
styles: [` |
|||
.tb-iot-hub-install-meta { |
|||
margin: 4px 0; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
font-size: 14px; |
|||
} |
|||
.tb-iot-hub-inline-spinner { |
|||
display: inline-block; |
|||
margin-right: 8px; |
|||
} |
|||
`]
|
|||
}) |
|||
export class TbIotHubInstallDialogComponent { |
|||
|
|||
item: MpItemVersionView; |
|||
typeTranslations = itemTypeTranslations; |
|||
state: InstallState = 'confirm'; |
|||
|
|||
constructor( |
|||
@Inject(MAT_DIALOG_DATA) public data: IotHubInstallDialogData, |
|||
private dialogRef: MatDialogRef<TbIotHubInstallDialogComponent>, |
|||
private store: Store<AppState>, |
|||
private translate: TranslateService |
|||
) { |
|||
this.item = data.item; |
|||
} |
|||
|
|||
install(): void { |
|||
this.state = 'installing'; |
|||
const versionId = this.item.id as string; |
|||
this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({ |
|||
next: () => { |
|||
this.state = 'success'; |
|||
this.store.dispatch(new ActionNotificationShow({ |
|||
message: this.translate.instant('iot-hub.install-success', { name: this.item.name }), |
|||
type: 'success', |
|||
duration: 3000 |
|||
})); |
|||
setTimeout(() => this.dialogRef.close(true), 1500); |
|||
}, |
|||
error: () => { |
|||
this.state = 'confirm'; |
|||
this.store.dispatch(new ActionNotificationShow({ |
|||
message: this.translate.instant('iot-hub.install-error', { name: this.item.name }), |
|||
type: 'error', |
|||
duration: 5000 |
|||
})); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(false); |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<!-- |
|||
|
|||
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-item-card" [class.tb-iot-hub-card-compact]="isCompactLayout()" (click)="onCardClick()"> |
|||
@if (!isCompactLayout()) { |
|||
<div class="tb-iot-hub-card-preview" [class]="getPreviewClass()"> |
|||
@if (getPreviewUrl(); as previewUrl) { |
|||
<img [src]="previewUrl" alt="" class="tb-iot-hub-preview-image"> |
|||
} @else { |
|||
<div class="tb-iot-hub-preview-placeholder" [style.background]="getCustomIconColor() ? getCustomIconColor() + '22' : null"> |
|||
<tb-icon [style.color]="getCustomIconColor()">{{ getPlaceholderIcon() }}</tb-icon> |
|||
</div> |
|||
} |
|||
@if (item.peOnly) { |
|||
<span class="tb-iot-hub-pe-badge">PE</span> |
|||
} |
|||
</div> |
|||
} |
|||
<div class="tb-iot-hub-card-content"> |
|||
@if (isCompactLayout()) { |
|||
<div class="tb-iot-hub-card-cf-header"> |
|||
<div class="tb-iot-hub-card-cf-icon" [style.background]="item.color || '#6366f1'"> |
|||
<tb-icon>{{ getPlaceholderIcon() }}</tb-icon> |
|||
</div> |
|||
<div class="tb-iot-hub-card-cf-title-area"> |
|||
<div class="tb-iot-hub-card-name-row"> |
|||
<h3 class="tb-iot-hub-card-name">{{ item.name }}</h3> |
|||
<span class="tb-iot-hub-card-version">v{{ item.version }}</span> |
|||
</div> |
|||
@if (getSubtypeLabel(); as subtypeLabel) { |
|||
<span class="tb-iot-hub-card-cf-type">{{ subtypeLabel }}</span> |
|||
} |
|||
</div> |
|||
@if (item.peOnly) { |
|||
<span class="tb-iot-hub-pe-badge tb-iot-hub-pe-badge-inline">PE</span> |
|||
} |
|||
</div> |
|||
} @else { |
|||
<div class="tb-iot-hub-card-name-row"> |
|||
<h3 class="tb-iot-hub-card-name">{{ item.name }}</h3> |
|||
<span class="tb-iot-hub-card-version">v{{ item.version }}</span> |
|||
</div> |
|||
@if (item.type === ItemType.WIDGET && getSubtypeLabel(); as subtypeLabel) { |
|||
<span class="tb-iot-hub-card-widget-type-label">{{ subtypeLabel }}</span> |
|||
} |
|||
} |
|||
@if (item.description) { |
|||
<p class="tb-iot-hub-card-description">{{ item.description }}</p> |
|||
} |
|||
@if (getNodes(); as nodes) { |
|||
@if (nodes.length) { |
|||
<div class="tb-iot-hub-card-nodes"> |
|||
@for (node of nodes | slice:0:maxVisibleNodes; track node.name) { |
|||
<span class="tb-iot-hub-card-node-chip" [ngClass]="getNodeColorClass(node)" [title]="getNodeLabel(node)">{{ node.name }}</span> |
|||
} |
|||
@if (getNodeCount() > maxVisibleNodes) { |
|||
<span class="tb-iot-hub-card-node-more">+{{ getNodeCount() - maxVisibleNodes }}</span> |
|||
} |
|||
</div> |
|||
} |
|||
} |
|||
@if (getCategoryLabels().length || getFirstUseCaseLabel()) { |
|||
<div class="tb-iot-hub-card-categories"> |
|||
@if (getCategoryLabels(); as labels) { |
|||
@if (labels.length) { |
|||
<span class="tb-iot-hub-card-category-chip">{{ labels[0] }}</span> |
|||
} |
|||
} |
|||
@if (getFirstUseCaseLabel(); as useCaseLabel) { |
|||
<span class="tb-iot-hub-card-use-case-chip">{{ useCaseLabel }}</span> |
|||
} |
|||
</div> |
|||
} |
|||
<div class="tb-iot-hub-card-footer"> |
|||
@if (showCreator) { |
|||
<tb-icon class="tb-iot-hub-card-author-icon">person</tb-icon><span class="tb-iot-hub-card-creator" (click)="onCreatorClick($event)">{{ item.creatorDisplayName }}</span> |
|||
@if (item.publishedTime) { |
|||
<span class="tb-iot-hub-card-dot">·</span> |
|||
} |
|||
} |
|||
@if (item.publishedTime) { |
|||
<span class="tb-iot-hub-card-published">{{ formatPublishedTime(item.publishedTime) }}</span> |
|||
} |
|||
<span class="tb-iot-hub-card-stats"> |
|||
<span class="tb-iot-hub-card-stat"> |
|||
<tb-icon>download</tb-icon> |
|||
{{ item.totalInstallCount | shortNumber }} |
|||
</span> |
|||
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">Install</button> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,351 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
:host { |
|||
display: block; |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-iot-hub-item-card { |
|||
border: 1px solid rgba(0, 0, 0, 0.1); |
|||
border-radius: 12px; |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
transition: box-shadow 0.2s, transform 0.2s; |
|||
background: white; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
&:hover { |
|||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); |
|||
transform: translateY(-2px); |
|||
} |
|||
|
|||
&.tb-iot-hub-card-compact { |
|||
border-color: #e5e7eb; |
|||
|
|||
.tb-iot-hub-card-content { |
|||
padding: 16px 18px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Preview area |
|||
.tb-iot-hub-card-preview { |
|||
height: 160px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: relative; |
|||
overflow: hidden; |
|||
|
|||
&.preview-widget, &.preview-dashboard { background: #f5f7fa; border-bottom: 1px solid #e5e7eb; } |
|||
&.preview-calculated-field { background: linear-gradient(135deg, #e3f2fd, #bbdefb); } |
|||
&.preview-rule-chain { background: linear-gradient(135deg, #f3e5f5, #e1bee7); } |
|||
&.preview-device { background: linear-gradient(135deg, #eceff1, #cfd8dc); } |
|||
} |
|||
|
|||
.tb-iot-hub-preview-image { |
|||
max-width: 100%; |
|||
max-height: 100%; |
|||
object-fit: contain; |
|||
padding: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-preview-placeholder { |
|||
width: 62px; |
|||
height: 62px; |
|||
border-radius: 16px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
tb-icon { |
|||
font-size: 36px; |
|||
width: 36px; |
|||
height: 36px; |
|||
} |
|||
|
|||
.preview-widget &, .preview-dashboard & { background: rgba(107, 114, 128, 0.1); tb-icon { color: #6b7280; } } |
|||
.preview-calculated-field & { background: rgba(25, 118, 210, 0.15); tb-icon { color: #1565c0; } } |
|||
.preview-rule-chain & { background: rgba(123, 31, 162, 0.15); tb-icon { color: #7b1fa2; } } |
|||
.preview-device & { background: rgba(69, 90, 100, 0.15); tb-icon { color: #455a64; } } |
|||
} |
|||
|
|||
.tb-iot-hub-pe-badge { |
|||
position: absolute; |
|||
top: 8px; |
|||
right: 8px; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
color: white; |
|||
padding: 2px 6px; |
|||
border-radius: 4px; |
|||
font-size: 11px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
// Content area |
|||
.tb-iot-hub-card-content { |
|||
padding: 12px 16px 12px; |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
// Compact header (CF / RC) |
|||
.tb-iot-hub-card-cf-header { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.tb-iot-hub-card-cf-icon { |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
|
|||
tb-icon { |
|||
font-size: 22px; |
|||
width: 22px; |
|||
height: 22px; |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-card-cf-title-area { |
|||
flex: 1; |
|||
min-width: 0; |
|||
|
|||
.tb-iot-hub-card-name-row { |
|||
margin-bottom: 2px; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-card-cf-type { |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
color: #6b7280; |
|||
} |
|||
|
|||
.tb-iot-hub-pe-badge-inline { |
|||
position: static; |
|||
flex-shrink: 0; |
|||
align-self: flex-start; |
|||
} |
|||
|
|||
// Name row |
|||
.tb-iot-hub-card-name-row { |
|||
display: flex; |
|||
align-items: baseline; |
|||
gap: 8px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.tb-iot-hub-card-name { |
|||
font-size: 16px; |
|||
font-weight: 700; |
|||
margin: 0; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
|
|||
.tb-iot-hub-card-version { |
|||
font-size: 13px; |
|||
color: rgba(0, 0, 0, 0.4); |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.tb-iot-hub-card-widget-type-label { |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
color: #6b7280; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
// Description |
|||
.tb-iot-hub-card-description { |
|||
font-size: 13px; |
|||
line-height: 1.4; |
|||
color: rgba(0, 0, 0, 0.55); |
|||
margin: 0 0 10px; |
|||
display: -webkit-box; |
|||
-webkit-line-clamp: 2; |
|||
-webkit-box-orient: vertical; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
// Node chips (Rule Chain) |
|||
.tb-iot-hub-card-nodes { |
|||
display: flex; |
|||
gap: 6px; |
|||
flex-wrap: nowrap; |
|||
overflow: hidden; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-card-node-chip { |
|||
display: inline-block; |
|||
padding: 2px 8px; |
|||
border-radius: 6px; |
|||
font-size: 11px; |
|||
line-height: 16px; |
|||
height: 20px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
min-width: 0; |
|||
max-width: 120px; |
|||
|
|||
&.node-enrichment { background: #f5fcd5; color: #4a7a00; } |
|||
&.node-filter { background: #fcfadb; color: #7a7200; } |
|||
&.node-transformation { background: #ddf2fc; color: #1a6d9e; } |
|||
&.node-action { background: #fde3e2; color: #9e2a27; } |
|||
&.node-analytics { background: #d5f6f4; color: #0e7a74; } |
|||
&.node-external { background: #fef1d8; color: #8a5d00; } |
|||
&.node-flow { background: #f3edfc; color: #5b3a9e; } |
|||
&.node-unknown { background: #f5f5f5; color: rgba(0, 0, 0, 0.54); } |
|||
} |
|||
|
|||
.tb-iot-hub-card-node-more { |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
color: rgba(0, 0, 0, 0.45); |
|||
padding: 3px 4px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
// Category / use case tags |
|||
.tb-iot-hub-card-categories { |
|||
display: flex; |
|||
gap: 4px; |
|||
flex-wrap: wrap; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.tb-iot-hub-card-category-chip { |
|||
display: inline-block; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
background: #eff6ff; |
|||
color: #1e6bb8; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.tb-iot-hub-card-use-case-chip { |
|||
display: inline-block; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
background: #f0fdf4; |
|||
color: #16a34a; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
// Footer |
|||
.tb-iot-hub-card-footer { |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 12px; |
|||
color: rgba(0, 0, 0, 0.45); |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
margin-top: auto; |
|||
margin-left: -16px; |
|||
margin-right: -16px; |
|||
margin-bottom: -12px; |
|||
padding: 12px 16px; |
|||
border-top: 1px solid rgba(0, 0, 0, 0.08); |
|||
|
|||
.tb-iot-hub-card-author-icon { |
|||
font-size: 14px; |
|||
width: 14px; |
|||
height: 14px; |
|||
flex-shrink: 0; |
|||
margin-right: 4px; |
|||
} |
|||
|
|||
.tb-iot-hub-card-creator { |
|||
color: #1976d2; |
|||
cursor: pointer; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-card-dot { |
|||
margin: 0 6px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.tb-iot-hub-card-published { |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.tb-iot-hub-card-stats { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-left: auto; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.tb-iot-hub-card-stat { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 3px; |
|||
font-size: 11px; |
|||
color: #9ca3af; |
|||
|
|||
tb-icon { |
|||
font-size: 14px; |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-card-install-btn { |
|||
padding: 5px 14px; |
|||
border-radius: 6px; |
|||
border: 1px solid #305680; |
|||
background: transparent; |
|||
color: #305680; |
|||
font-size: 12px; |
|||
font-weight: 600; |
|||
cursor: pointer; |
|||
white-space: nowrap; |
|||
transition: all 0.15s; |
|||
|
|||
&:hover { |
|||
background: #305680; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,204 @@ |
|||
///
|
|||
/// 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, EventEmitter, Input, Output } from '@angular/core'; |
|||
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo, nodeComponentTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-item-card', |
|||
standalone: false, |
|||
templateUrl: './iot-hub-item-card.component.html', |
|||
styleUrls: ['./iot-hub-item-card.component.scss'] |
|||
}) |
|||
export class TbIotHubItemCardComponent { |
|||
|
|||
readonly ItemType = ItemType; |
|||
readonly maxVisibleCategories = 3; |
|||
readonly maxVisibleNodes = 2; |
|||
|
|||
@Input() item: MpItemVersionView; |
|||
@Input() showCreator = true; |
|||
@Input() showTypeChip = true; |
|||
@Output() cardClick = new EventEmitter<MpItemVersionView>(); |
|||
@Output() creatorClick = new EventEmitter<string>(); |
|||
@Output() installClick = new EventEmitter<MpItemVersionView>(); |
|||
|
|||
typeTranslations = itemTypeTranslations; |
|||
|
|||
constructor( |
|||
private translate: TranslateService, |
|||
private iotHubApiService: IotHubApiService |
|||
) {} |
|||
|
|||
isCompactLayout(): boolean { |
|||
return this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN; |
|||
} |
|||
|
|||
getPreviewUrl(): string | null { |
|||
if (!this.item.image) { |
|||
return null; |
|||
} |
|||
const url = this.item.image.split('?')[0]; |
|||
const resolved = url.endsWith('/preview') ? this.item.image : `${url}/preview`; |
|||
return this.iotHubApiService.resolveResourceUrl(resolved); |
|||
} |
|||
|
|||
getPreviewClass(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: return 'preview-widget'; |
|||
case ItemType.DASHBOARD: return 'preview-dashboard'; |
|||
case ItemType.CALCULATED_FIELD: return 'preview-calculated-field'; |
|||
case ItemType.RULE_CHAIN: return 'preview-rule-chain'; |
|||
case ItemType.DEVICE: return 'preview-device'; |
|||
default: return ''; |
|||
} |
|||
} |
|||
|
|||
getPlaceholderIcon(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: return 'widgets'; |
|||
case ItemType.DASHBOARD: return 'dashboard'; |
|||
case ItemType.CALCULATED_FIELD: |
|||
return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions'; |
|||
case ItemType.RULE_CHAIN: |
|||
return this.item.icon || (this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'router' : 'device_hub'); |
|||
case ItemType.DEVICE: return 'memory'; |
|||
default: return 'extension'; |
|||
} |
|||
} |
|||
|
|||
getCustomIconColor(): string | null { |
|||
if ((this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN) && this.item.color) { |
|||
return this.item.color; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
getSubtypeLabel(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: { |
|||
const wt = this.item.dataDescriptor?.widgetType; |
|||
const key = wt ? widgetTypeTranslations.get(wt) : null; |
|||
return key ? this.translate.instant(key) : wt || ''; |
|||
} |
|||
case ItemType.CALCULATED_FIELD: { |
|||
const cfType = this.item.dataDescriptor?.cfType; |
|||
const key = cfType ? cfTypeTranslations.get(cfType) : null; |
|||
return key ? this.translate.instant(key) : cfType || ''; |
|||
} |
|||
case ItemType.RULE_CHAIN: { |
|||
const rcType = this.item.dataDescriptor?.ruleChainType; |
|||
const key = rcType ? ruleChainTypeTranslations.get(rcType) : null; |
|||
return key ? this.translate.instant(key) : rcType || ''; |
|||
} |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
getSubtypeColorClass(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: |
|||
return 'wt-' + (this.item.dataDescriptor?.widgetType || 'static'); |
|||
case ItemType.CALCULATED_FIELD: |
|||
switch (this.item.dataDescriptor?.cfType) { |
|||
case 'SIMPLE': return 'cf-simple'; |
|||
case 'SCRIPT': return 'cf-script'; |
|||
case 'GEOFENCING': return 'cf-geofencing'; |
|||
case 'ALARM': return 'cf-alarm'; |
|||
case 'PROPAGATION': return 'cf-propagation'; |
|||
case 'RELATED_ENTITIES_AGGREGATION': return 'cf-related-agg'; |
|||
case 'ENTITY_AGGREGATION': return 'cf-entity-agg'; |
|||
default: return 'cf-simple'; |
|||
} |
|||
case ItemType.RULE_CHAIN: |
|||
return this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'rc-edge' : 'rc-core'; |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
getCategoryLabels(): string[] { |
|||
if (!this.item.categories?.length) { |
|||
return []; |
|||
} |
|||
const categoryMap = getCategoriesForType(this.item.type); |
|||
return this.item.categories.map(c => { |
|||
const key = categoryMap.get(c); |
|||
return key ? this.translate.instant(key) : c; |
|||
}); |
|||
} |
|||
|
|||
getFirstUseCaseLabel(): string | null { |
|||
if (!this.item.useCases?.length) { |
|||
return null; |
|||
} |
|||
const key = useCaseTranslations.get(this.item.useCases[0] as any); |
|||
return key ? this.translate.instant(key) : this.item.useCases[0]; |
|||
} |
|||
|
|||
getNodes(): NodeInfo[] { |
|||
if (this.item.type !== ItemType.RULE_CHAIN) { |
|||
return []; |
|||
} |
|||
return this.item.dataDescriptor?.nodes || []; |
|||
} |
|||
|
|||
getNodeCount(): number { |
|||
return this.item.dataDescriptor?.nodeCount || 0; |
|||
} |
|||
|
|||
getNodeLabel(node: NodeInfo): string { |
|||
const key = nodeComponentTypeTranslations.get(node.type); |
|||
return key ? this.translate.instant(key) : node.type; |
|||
} |
|||
|
|||
getNodeColorClass(node: NodeInfo): string { |
|||
switch (node.type) { |
|||
case 'ENRICHMENT': return 'node-enrichment'; |
|||
case 'FILTER': return 'node-filter'; |
|||
case 'TRANSFORMATION': return 'node-transformation'; |
|||
case 'ACTION': return 'node-action'; |
|||
case 'ANALYTICS': return 'node-analytics'; |
|||
case 'EXTERNAL': return 'node-external'; |
|||
case 'FLOW': return 'node-flow'; |
|||
default: return 'node-unknown'; |
|||
} |
|||
} |
|||
|
|||
onCardClick(): void { |
|||
this.cardClick.emit(this.item); |
|||
} |
|||
|
|||
onInstallClick(event: MouseEvent): void { |
|||
event.stopPropagation(); |
|||
this.installClick.emit(this.item); |
|||
} |
|||
|
|||
onCreatorClick(event: MouseEvent): void { |
|||
event.stopPropagation(); |
|||
this.creatorClick.emit(this.item.creatorId); |
|||
} |
|||
|
|||
formatPublishedTime(timestamp: number): string { |
|||
const date = new Date(timestamp); |
|||
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
<!-- |
|||
|
|||
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 mat-dialog-content class="tb-iot-hub-detail-dialog"> |
|||
<!-- Header (sticky) --> |
|||
<div class="dlg-header"> |
|||
<div class="dlg-header-top"> |
|||
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) { |
|||
<div class="dlg-icon" [style.background]="item.color || '#6366f1'"> |
|||
<tb-icon>{{ getCompactIcon() }}</tb-icon> |
|||
</div> |
|||
} |
|||
<div class="dlg-header-info"> |
|||
<div class="dlg-title-row"> |
|||
<span class="dlg-title">{{ item.name }}</span> |
|||
<span class="dlg-version">v{{ item.version }}</span> |
|||
@if (getSubtypeLabel(); as subtypeLabel) { |
|||
<span class="dlg-type-badge" [ngClass]="getSubtypeColorClass()">{{ subtypeLabel }}</span> |
|||
} |
|||
</div> |
|||
<div class="dlg-subtitle"> |
|||
<span>{{ typeTranslations.get(item.type) | translate }}</span> |
|||
<span class="dlg-sep">·</span> |
|||
<span>{{ 'iot-hub.by-creator' | translate:{ name: '' } }}</span> |
|||
<span class="dlg-creator-link" (click)="navigateToCreator()">{{ item.creatorDisplayName }}</span> |
|||
@if (item.publishedTime) { |
|||
<span class="dlg-sep">·</span> |
|||
<span>{{ item.publishedTime | date:'mediumDate' }}</span> |
|||
} |
|||
</div> |
|||
</div> |
|||
<button mat-icon-button class="dlg-close" (click)="close()" type="button"> |
|||
<tb-icon>close</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Scrollable body --> |
|||
<div class="dlg-body"> |
|||
<!-- Widget: image + description side by side --> |
|||
@if (item.type === ItemType.WIDGET) { |
|||
<div class="dlg-hero-split"> |
|||
<div class="dlg-hero-image"> |
|||
@if (getPreviewUrl(); as url) { |
|||
<img [src]="url" alt=""> |
|||
} @else { |
|||
<div class="tb-iot-hub-preview-placeholder"> |
|||
<tb-icon>widgets</tb-icon> |
|||
</div> |
|||
} |
|||
</div> |
|||
@if (item.description) { |
|||
<div class="dlg-hero-desc">{{ item.description }}</div> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- Dashboard: full-width image + description below --> |
|||
@if (item.type === ItemType.DASHBOARD) { |
|||
<div class="dlg-hero-full"> |
|||
<div class="dlg-hero-full-image"> |
|||
@if (getPreviewUrl(); as url) { |
|||
<img [src]="url" alt=""> |
|||
} @else { |
|||
<div class="tb-iot-hub-preview-placeholder"> |
|||
<tb-icon>dashboard</tb-icon> |
|||
</div> |
|||
} |
|||
</div> |
|||
@if (item.description) { |
|||
<div class="dlg-hero-full-desc">{{ item.description }}</div> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- Stats bar --> |
|||
<div class="dlg-stats-bar"> |
|||
<span class="dlg-stat"> |
|||
<tb-icon>download</tb-icon> |
|||
<strong>{{ item.totalInstallCount | shortNumber }}</strong> {{ 'iot-hub.installs' | translate }} |
|||
</span> |
|||
<span class="dlg-stat"> |
|||
<tb-icon>{{ getTypeIcon() }}</tb-icon> |
|||
{{ typeTranslations.get(item.type) | translate }} |
|||
</span> |
|||
@if (item.peOnly) { |
|||
<span class="dlg-pe-label">PE</span> |
|||
} |
|||
</div> |
|||
|
|||
<!-- Device preview --> |
|||
@if (item.type === ItemType.DEVICE) { |
|||
<div class="dlg-device-preview"> |
|||
@if (getPreviewUrl(); as url) { |
|||
<img [src]="url" alt=""> |
|||
} @else { |
|||
<div class="tb-iot-hub-preview-placeholder"> |
|||
<tb-icon>memory</tb-icon> |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- CF/RC: description block --> |
|||
@if (item.description && item.type !== ItemType.WIDGET && item.type !== ItemType.DASHBOARD) { |
|||
<div class="dlg-desc-block">{{ item.description }}</div> |
|||
} |
|||
|
|||
<!-- Node chips (Rule Chain) --> |
|||
@if (item.type === ItemType.RULE_CHAIN && getNodes().length) { |
|||
<div class="dlg-node-chips"> |
|||
@for (node of getNodes(); track node.name) { |
|||
<span class="dlg-node-chip" [ngClass]="'dlg-node-' + node.type.toLowerCase()" [title]="getNodeLabel(node)">{{ node.name }}</span> |
|||
} |
|||
@if (getNodeCount() > getNodes().length) { |
|||
<span class="dlg-node-more">+{{ getNodeCount() - getNodes().length }} {{ 'iot-hub.more' | translate }}</span> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- Categories & Use Cases --> |
|||
@if (item.categories?.length || item.useCases?.length) { |
|||
<div class="dlg-meta-section"> |
|||
@if (item.categories?.length) { |
|||
<div class="dlg-meta-group"> |
|||
<span class="dlg-meta-label">{{ 'iot-hub.categories' | translate }}</span> |
|||
<div class="dlg-meta-chips"> |
|||
@for (cat of item.categories; track cat) { |
|||
<span class="dlg-meta-chip dlg-meta-chip-category">{{ getCategoryLabel(cat) }}</span> |
|||
} |
|||
</div> |
|||
</div> |
|||
} |
|||
@if (item.useCases?.length) { |
|||
<div class="dlg-meta-group"> |
|||
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span> |
|||
<div class="dlg-meta-chips"> |
|||
@for (uc of item.useCases; track uc) { |
|||
<span class="dlg-meta-chip dlg-meta-chip-usecase">{{ getUseCaseLabel(uc) }}</span> |
|||
} |
|||
</div> |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- Readme / Changelog --> |
|||
@if (readmeContent || item.changelog) { |
|||
<div class="dlg-content"> |
|||
@if (readmeContent) { |
|||
<tb-markdown [data]="readmeContent"></tb-markdown> |
|||
} |
|||
@if (item.changelog) { |
|||
<h3>{{ 'iot-hub.changelog' | translate }}</h3> |
|||
<tb-markdown [data]="item.changelog"></tb-markdown> |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
|
|||
<!-- Footer --> |
|||
<div class="dlg-footer"> |
|||
<button class="dlg-install-btn" (click)="install()"> |
|||
{{ 'iot-hub.install-item-type' | translate:{ type: getTypeLabel() } }} |
|||
</button> |
|||
<div class="dlg-creator-info"> |
|||
<span>{{ 'iot-hub.created-by' | translate:{ name: item.creatorDisplayName } }}</span> |
|||
<span class="dlg-sep">·</span> |
|||
<span class="dlg-become-creator" (click)="openSignup()"> |
|||
{{ 'iot-hub.become-a-creator' | translate }} |
|||
</span> |
|||
</div> |
|||
</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. |
|||
*/ |
|||
|
|||
:host-context(.tb-default .tb-dialog .mat-mdc-dialog-component-host) { |
|||
@media (min-width: 600px) { |
|||
.mat-mdc-dialog-content { |
|||
width: 800px; |
|||
max-height: 80vh; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-iot-hub-detail-dialog { |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 0 !important; |
|||
--mat-dialog-supporting-text-line-height: 1.5; |
|||
} |
|||
|
|||
// Header |
|||
.dlg-header { |
|||
padding: 20px 24px 16px; |
|||
border-bottom: 1px solid #e5e7eb; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.dlg-header-top { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 14px; |
|||
} |
|||
|
|||
.dlg-icon { |
|||
width: 48px; |
|||
height: 48px; |
|||
border-radius: 12px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
|
|||
tb-icon { |
|||
font-size: 26px; |
|||
width: 26px; |
|||
height: 26px; |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
.dlg-header-info { |
|||
flex: 1; |
|||
min-width: 0; |
|||
} |
|||
|
|||
.dlg-title-row { |
|||
display: flex; |
|||
align-items: baseline; |
|||
gap: 8px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.dlg-title { |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
color: #1a1a2e; |
|||
} |
|||
|
|||
.dlg-version { |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
color: #9ca3af; |
|||
} |
|||
|
|||
.dlg-type-badge { |
|||
padding: 2px 10px; |
|||
border-radius: 12px; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
|
|||
&.wt-timeseries { background: #e3f2fd; color: #1565c0; } |
|||
&.wt-latest { background: #e8f5e9; color: #2e7d32; } |
|||
&.wt-rpc { background: #f3e5f5; color: #7b1fa2; } |
|||
&.wt-alarm { background: #ffebee; color: #c62828; } |
|||
&.wt-static { background: #eceff1; color: #455a64; } |
|||
&.cf-simple { background: #e3f2fd; color: #1565c0; } |
|||
&.cf-script { background: #f3e5f5; color: #6a1b9a; } |
|||
&.cf-geofencing { background: #e8f5e9; color: #2e7d32; } |
|||
&.cf-alarm { background: #ffebee; color: #c62828; } |
|||
&.cf-propagation { background: #e0f2f1; color: #00695c; } |
|||
&.cf-related-agg { background: #e8eaf6; color: #283593; } |
|||
&.cf-entity-agg { background: #fff3e0; color: #e65100; } |
|||
&.rc-core { background: #f3e5f5; color: #7b1fa2; } |
|||
&.rc-edge { background: #e0f2f1; color: #00695c; } |
|||
} |
|||
|
|||
.dlg-subtitle { |
|||
font-size: 12px; |
|||
color: #6b7280; |
|||
margin-top: 2px; |
|||
} |
|||
|
|||
.dlg-creator-link { |
|||
color: #1976d2; |
|||
text-decoration: none; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
|
|||
.dlg-close { |
|||
flex-shrink: 0; |
|||
margin: -8px -8px 0 0; |
|||
} |
|||
|
|||
.dlg-sep { |
|||
color: #9ca3af; |
|||
padding: 0 2px; |
|||
} |
|||
|
|||
// Hero split (widget: image + description side by side) |
|||
.dlg-hero-split { |
|||
display: flex; |
|||
gap: 20px; |
|||
padding: 20px 24px; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
} |
|||
|
|||
.dlg-hero-image { |
|||
flex: 0 0 48%; |
|||
background: #f5f7fa; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 140px; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: contain; |
|||
padding: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-preview-placeholder tb-icon { |
|||
font-size: 64px; |
|||
width: 64px; |
|||
height: 64px; |
|||
opacity: 0.3; |
|||
} |
|||
} |
|||
|
|||
.dlg-hero-desc { |
|||
flex: 1; |
|||
font-size: 13px; |
|||
color: #4b5563; |
|||
line-height: 1.6; |
|||
|
|||
p { |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
// Dashboard: full-width image + description below |
|||
.dlg-hero-full { |
|||
padding: 20px 24px; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
} |
|||
|
|||
.dlg-hero-full-image { |
|||
background: #f5f7fa; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 200px; |
|||
max-height: 300px; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: contain; |
|||
padding: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-preview-placeholder tb-icon { |
|||
font-size: 64px; |
|||
width: 64px; |
|||
height: 64px; |
|||
opacity: 0.3; |
|||
} |
|||
} |
|||
|
|||
.dlg-hero-full-desc { |
|||
margin-top: 12px; |
|||
font-size: 13px; |
|||
color: #4b5563; |
|||
line-height: 1.6; |
|||
} |
|||
|
|||
// Stats bar |
|||
.dlg-stats-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
padding: 10px 24px; |
|||
background: #f9fafb; |
|||
font-size: 12px; |
|||
color: #6b7280; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
} |
|||
|
|||
.dlg-stat { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
|
|||
tb-icon { |
|||
font-size: 16px; |
|||
width: 16px; |
|||
height: 16px; |
|||
} |
|||
|
|||
strong { |
|||
color: #1a1a2e; |
|||
} |
|||
} |
|||
|
|||
.dlg-pe-label { |
|||
display: inline-block; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
color: white; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 11px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
// Device preview |
|||
.dlg-device-preview { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 160px; |
|||
background: #f5f7fa; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
|
|||
img { |
|||
max-width: 100%; |
|||
max-height: 160px; |
|||
object-fit: contain; |
|||
padding: 8px; |
|||
} |
|||
|
|||
.tb-iot-hub-preview-placeholder tb-icon { |
|||
font-size: 64px; |
|||
width: 64px; |
|||
height: 64px; |
|||
opacity: 0.3; |
|||
} |
|||
} |
|||
|
|||
// Description block (CF/RC) |
|||
.dlg-desc-block { |
|||
padding: 16px 24px; |
|||
font-size: 13px; |
|||
color: #4b5563; |
|||
line-height: 1.6; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
} |
|||
|
|||
// Node chips (Rule Chain) |
|||
.dlg-node-chips { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 6px; |
|||
padding: 12px 24px; |
|||
border-bottom: 1px solid #f3f4f6; |
|||
} |
|||
|
|||
.dlg-node-chip { |
|||
padding: 4px 12px; |
|||
border-radius: 14px; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
border: 1px solid; |
|||
|
|||
&.dlg-node-enrichment { background: #f5fcd5; color: #4a7a00; border-color: #d4e6a0; } |
|||
&.dlg-node-filter { background: #fcfadb; color: #7a7200; border-color: #e8e4a0; } |
|||
&.dlg-node-transformation { background: #ddf2fc; color: #1a6d9e; border-color: #a8d8f0; } |
|||
&.dlg-node-action { background: #fde3e2; color: #9e2a27; border-color: #f0b8b6; } |
|||
&.dlg-node-analytics { background: #d5f6f4; color: #0e7a74; border-color: #a0e0dc; } |
|||
&.dlg-node-external { background: #fef1d8; color: #8a5d00; border-color: #f0d8a0; } |
|||
&.dlg-node-flow { background: #f3edfc; color: #5b3a9e; border-color: #d0c4f0; } |
|||
&.dlg-node-unknown { background: #f5f5f5; color: rgba(0, 0, 0, 0.54); border-color: #e0e0e0; } |
|||
} |
|||
|
|||
.dlg-node-more { |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
align-self: center; |
|||
} |
|||
|
|||
// Categories & Use Cases |
|||
.dlg-meta-section { |
|||
display: flex; |
|||
gap: 40px; |
|||
padding: 14px 24px; |
|||
border-bottom: 1px solid #e5e7eb; |
|||
} |
|||
|
|||
.dlg-meta-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 6px; |
|||
} |
|||
|
|||
.dlg-meta-label { |
|||
font-size: 10px; |
|||
font-weight: 600; |
|||
color: #9ca3af; |
|||
text-transform: uppercase; |
|||
letter-spacing: 0.5px; |
|||
} |
|||
|
|||
.dlg-meta-chips { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 5px; |
|||
} |
|||
|
|||
.dlg-meta-chip { |
|||
padding: 3px 10px; |
|||
border-radius: 4px; |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
border: 1px solid #e5e7eb; |
|||
color: #4b5563; |
|||
background: #fff; |
|||
} |
|||
|
|||
// Scrollable body |
|||
.dlg-body { |
|||
overflow-y: auto; |
|||
flex: 1; |
|||
min-height: 0; |
|||
} |
|||
|
|||
.dlg-content { |
|||
padding: 0; |
|||
|
|||
h3 { |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
margin: 20px 0 8px; |
|||
} |
|||
} |
|||
|
|||
// Footer |
|||
.dlg-footer { |
|||
padding: 16px 24px; |
|||
border-top: 1px solid #e5e7eb; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 10px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.dlg-install-btn { |
|||
width: 100%; |
|||
max-width: 400px; |
|||
padding: 12px 24px; |
|||
border: none; |
|||
border-radius: 8px; |
|||
background: #305680; |
|||
color: #fff; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
cursor: pointer; |
|||
transition: background 0.15s; |
|||
|
|||
&:hover { |
|||
background: #264466; |
|||
} |
|||
} |
|||
|
|||
.dlg-creator-info { |
|||
font-size: 12px; |
|||
color: #9ca3af; |
|||
} |
|||
|
|||
.dlg-become-creator { |
|||
color: #305680; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
text-decoration: underline; |
|||
} |
|||
} |
|||
@ -0,0 +1,246 @@ |
|||
///
|
|||
/// 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, Inject } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Router } from '@angular/router'; |
|||
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, nodeComponentTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; |
|||
import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
|||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; |
|||
|
|||
export interface IotHubItemDetailDialogData { |
|||
item: MpItemVersionView; |
|||
iotHubApiService: IotHubApiService; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-iot-hub-item-detail-dialog', |
|||
standalone: false, |
|||
templateUrl: './iot-hub-item-detail-dialog.component.html', |
|||
styleUrls: ['./iot-hub-item-detail-dialog.component.scss'] |
|||
}) |
|||
export class TbIotHubItemDetailDialogComponent { |
|||
|
|||
readonly ItemType = ItemType; |
|||
item: MpItemVersionView; |
|||
typeTranslations = itemTypeTranslations; |
|||
readmeContent: string = ''; |
|||
|
|||
private categoryMap: Map<string, string>; |
|||
private useCaseMap = useCaseTranslations; |
|||
|
|||
constructor( |
|||
@Inject(MAT_DIALOG_DATA) public data: IotHubItemDetailDialogData, |
|||
private dialogRef: MatDialogRef<TbIotHubItemDetailDialogComponent>, |
|||
private dialog: MatDialog, |
|||
private router: Router, |
|||
private translate: TranslateService |
|||
) { |
|||
this.item = data.item; |
|||
this.categoryMap = getCategoriesForType(this.item.type); |
|||
this.loadReadme(); |
|||
} |
|||
|
|||
getCategoryLabel(key: string): string { |
|||
const translationKey = this.categoryMap.get(key); |
|||
return translationKey ? this.translate.instant(translationKey) : key; |
|||
} |
|||
|
|||
getUseCaseLabel(key: string): string { |
|||
const translationKey = this.useCaseMap.get(key as any); |
|||
return translationKey ? this.translate.instant(translationKey) : key; |
|||
} |
|||
|
|||
getPreviewUrl(): string | null { |
|||
return this.item.image ? this.data.iotHubApiService.resolveResourceUrl(this.item.image) : null; |
|||
} |
|||
|
|||
getTypeChipClass(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: return 'type-widget'; |
|||
case ItemType.DASHBOARD: return 'type-dashboard'; |
|||
case ItemType.CALCULATED_FIELD: return 'type-calc-field'; |
|||
case ItemType.RULE_CHAIN: return 'type-rule-chain'; |
|||
case ItemType.DEVICE: return 'type-device'; |
|||
default: return ''; |
|||
} |
|||
} |
|||
|
|||
getTypeLabel(): string { |
|||
const key = this.typeTranslations.get(this.item.type); |
|||
return key ? this.translate.instant(key) : ''; |
|||
} |
|||
|
|||
getTypeIcon(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.WIDGET: return 'widgets'; |
|||
case ItemType.DASHBOARD: return 'dashboard'; |
|||
case ItemType.CALCULATED_FIELD: return 'functions'; |
|||
case ItemType.RULE_CHAIN: return 'account_tree'; |
|||
case ItemType.DEVICE: return 'memory'; |
|||
default: return 'category'; |
|||
} |
|||
} |
|||
|
|||
getCompactIcon(): string { |
|||
if (this.item.icon) { |
|||
return this.item.icon; |
|||
} |
|||
if (this.item.type === ItemType.CALCULATED_FIELD) { |
|||
const cfType = this.item.dataDescriptor?.cfType; |
|||
return cfTypeIcons.get(cfType) || 'functions'; |
|||
} |
|||
switch (this.item.dataDescriptor?.ruleChainType) { |
|||
case 'CORE': return 'device_hub'; |
|||
case 'EDGE': return 'router'; |
|||
default: return 'account_tree'; |
|||
} |
|||
} |
|||
|
|||
getCustomColor(): string | null { |
|||
return this.item.color || null; |
|||
} |
|||
|
|||
getCompactIconColorClass(): string { |
|||
if (this.item.color) { |
|||
return ''; |
|||
} |
|||
if (this.item.type === ItemType.CALCULATED_FIELD) { |
|||
switch (this.item.dataDescriptor?.cfType) { |
|||
case 'SIMPLE': return 'cf-simple'; |
|||
case 'SCRIPT': return 'cf-script'; |
|||
case 'GEOFENCING': return 'cf-geofencing'; |
|||
case 'ALARM': return 'cf-alarm'; |
|||
case 'PROPAGATION': return 'cf-propagation'; |
|||
case 'RELATED_ENTITIES_AGGREGATION': return 'cf-related-agg'; |
|||
case 'ENTITY_AGGREGATION': return 'cf-entity-agg'; |
|||
default: return 'cf-entity-agg'; |
|||
} |
|||
} |
|||
switch (this.item.dataDescriptor?.ruleChainType) { |
|||
case 'CORE': return 'rc-core'; |
|||
case 'EDGE': return 'rc-edge'; |
|||
default: return 'rc-core'; |
|||
} |
|||
} |
|||
|
|||
getCompactSubtypeLabel(): string { |
|||
if (this.item.type === ItemType.CALCULATED_FIELD) { |
|||
const cfType = this.item.dataDescriptor?.cfType; |
|||
const key = cfType ? cfTypeTranslations.get(cfType) : null; |
|||
return key ? this.translate.instant(key) : cfType || ''; |
|||
} |
|||
const rcType = this.item.dataDescriptor?.ruleChainType; |
|||
const key = rcType ? ruleChainTypeTranslations.get(rcType) : null; |
|||
return key ? this.translate.instant(key) : rcType || ''; |
|||
} |
|||
|
|||
getSubtypeLabel(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.CALCULATED_FIELD: |
|||
return this.getCompactSubtypeLabel(); |
|||
case ItemType.RULE_CHAIN: |
|||
return this.getCompactSubtypeLabel(); |
|||
case ItemType.WIDGET: { |
|||
const wt = this.item.dataDescriptor?.widgetType; |
|||
const key = wt ? widgetTypeTranslations.get(wt) : null; |
|||
return key ? this.translate.instant(key) : ''; |
|||
} |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
getSubtypeColorClass(): string { |
|||
switch (this.item.type) { |
|||
case ItemType.CALCULATED_FIELD: |
|||
switch (this.item.dataDescriptor?.cfType) { |
|||
case 'SIMPLE': return 'cf-simple'; |
|||
case 'SCRIPT': return 'cf-script'; |
|||
case 'GEOFENCING': return 'cf-geofencing'; |
|||
case 'ALARM': return 'cf-alarm'; |
|||
case 'PROPAGATION': return 'cf-propagation'; |
|||
case 'RELATED_ENTITIES_AGGREGATION': return 'cf-related-agg'; |
|||
case 'ENTITY_AGGREGATION': return 'cf-entity-agg'; |
|||
default: return 'cf-simple'; |
|||
} |
|||
case ItemType.RULE_CHAIN: |
|||
return this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'rc-edge' : 'rc-core'; |
|||
case ItemType.WIDGET: |
|||
switch (this.item.dataDescriptor?.widgetType) { |
|||
case 'timeseries': return 'wt-timeseries'; |
|||
case 'latest': return 'wt-latest'; |
|||
case 'rpc': return 'wt-rpc'; |
|||
case 'alarm': return 'wt-alarm'; |
|||
case 'static': return 'wt-static'; |
|||
default: return 'wt-static'; |
|||
} |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
getNodeLabel(node: NodeInfo): string { |
|||
const key = nodeComponentTypeTranslations.get(node.type); |
|||
const typeLabel = key ? this.translate.instant(key) : node.type; |
|||
return `${node.name} (${typeLabel})`; |
|||
} |
|||
|
|||
getNodes(): NodeInfo[] { |
|||
return this.item.dataDescriptor?.nodes || []; |
|||
} |
|||
|
|||
getNodeCount(): number { |
|||
return this.item.dataDescriptor?.nodeCount || 0; |
|||
} |
|||
|
|||
install(): void { |
|||
this.dialog.open(TbIotHubInstallDialogComponent, { |
|||
panelClass: ['tb-dialog'], |
|||
data: { |
|||
item: this.item, |
|||
iotHubApiService: this.data.iotHubApiService |
|||
} as IotHubInstallDialogData |
|||
}); |
|||
} |
|||
|
|||
openSignup(): void { |
|||
window.open('https://iothub.thingsboard.io/signup', '_blank'); |
|||
} |
|||
|
|||
navigateToCreator(): void { |
|||
this.dialogRef.close(); |
|||
this.router.navigate(['/iot-hub/creator', this.item.creatorId]); |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
private loadReadme(): void { |
|||
const versionId = this.item.id as string; |
|||
this.data.iotHubApiService.getVersionReadme(versionId, { ignoreLoading: true }).subscribe( |
|||
content => this.readmeContent = this.prefixResourceUrls(content || '') |
|||
); |
|||
} |
|||
|
|||
private prefixResourceUrls(markdown: string): string { |
|||
const baseUrl = this.data.iotHubApiService.baseUrl; |
|||
return markdown.replace(/(\(|")(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`); |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
///
|
|||
/// 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 { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
|
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { TbIotHubBrowseComponent } from './iot-hub-browse.component'; |
|||
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: 'iot-hub', |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN], |
|||
breadcrumb: { |
|||
label: 'iot-hub.iot-hub', |
|||
icon: 'store' |
|||
} |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: TbIotHubBrowseComponent, |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN], |
|||
title: 'iot-hub.browse' |
|||
} |
|||
}, |
|||
{ |
|||
path: 'creator/:creatorId', |
|||
component: TbIotHubCreatorProfileComponent, |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN], |
|||
title: 'iot-hub.creator-profile', |
|||
breadcrumb: { |
|||
label: 'iot-hub.creator-profile', |
|||
icon: 'person' |
|||
} |
|||
} |
|||
} |
|||
] |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule] |
|||
}) |
|||
export class IotHubRoutingModule { } |
|||
@ -0,0 +1,41 @@ |
|||
///
|
|||
/// 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 { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { IotHubRoutingModule } from './iot-hub-routing.module'; |
|||
import { TbIotHubBrowseComponent } from './iot-hub-browse.component'; |
|||
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; |
|||
import { TbIotHubItemDetailDialogComponent } from './iot-hub-item-detail-dialog.component'; |
|||
import { TbIotHubInstallDialogComponent } from './iot-hub-install-dialog.component'; |
|||
import { TbIotHubItemCardComponent } from './iot-hub-item-card.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
TbIotHubBrowseComponent, |
|||
TbIotHubCreatorProfileComponent, |
|||
TbIotHubItemDetailDialogComponent, |
|||
TbIotHubInstallDialogComponent, |
|||
TbIotHubItemCardComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
IotHubRoutingModule |
|||
] |
|||
}) |
|||
export class IotHubModule { } |
|||
@ -0,0 +1,29 @@ |
|||
///
|
|||
/// 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.
|
|||
///
|
|||
|
|||
export interface CreatorView { |
|||
id: string; |
|||
createdTime: number; |
|||
displayName: string; |
|||
contactEmail: string; |
|||
website: string; |
|||
description: string; |
|||
avatarUrl: string; |
|||
githubUrl: string; |
|||
linkedinUrl: string; |
|||
twitterUrl: string; |
|||
publishedCount: number; |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
///
|
|||
/// 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.
|
|||
///
|
|||
|
|||
export enum ItemType { |
|||
WIDGET = 'WIDGET', |
|||
DASHBOARD = 'DASHBOARD', |
|||
CALCULATED_FIELD = 'CALCULATED_FIELD', |
|||
RULE_CHAIN = 'RULE_CHAIN', |
|||
DEVICE = 'DEVICE' |
|||
} |
|||
|
|||
export const itemTypeTranslations = new Map<ItemType, string>( |
|||
[ |
|||
[ItemType.WIDGET, 'item.type-widget'], |
|||
[ItemType.DASHBOARD, 'item.type-dashboard'], |
|||
[ItemType.CALCULATED_FIELD, 'item.type-calculated-field'], |
|||
[ItemType.RULE_CHAIN, 'item.type-rule-chain'], |
|||
[ItemType.DEVICE, 'item.type-device'] |
|||
] |
|||
); |
|||
|
|||
export enum WidgetCategory { |
|||
CHARTS_GRAPHS = 'CHARTS_GRAPHS', |
|||
GAUGES_INDICATORS = 'GAUGES_INDICATORS', |
|||
CONTROLS = 'CONTROLS', |
|||
MAPS_LOCATION = 'MAPS_LOCATION', |
|||
TABLES_LISTS = 'TABLES_LISTS', |
|||
CARDS_INFO = 'CARDS_INFO', |
|||
SCADA = 'SCADA', |
|||
INPUT_FORMS = 'INPUT_FORMS' |
|||
} |
|||
|
|||
export const widgetCategoryTranslations = new Map<WidgetCategory, string>([ |
|||
[WidgetCategory.CHARTS_GRAPHS, 'iot-hub.category.charts-graphs'], |
|||
[WidgetCategory.GAUGES_INDICATORS, 'iot-hub.category.gauges-indicators'], |
|||
[WidgetCategory.CONTROLS, 'iot-hub.category.controls'], |
|||
[WidgetCategory.MAPS_LOCATION, 'iot-hub.category.maps-location'], |
|||
[WidgetCategory.TABLES_LISTS, 'iot-hub.category.tables-lists'], |
|||
[WidgetCategory.CARDS_INFO, 'iot-hub.category.cards-info'], |
|||
[WidgetCategory.SCADA, 'iot-hub.category.scada'], |
|||
[WidgetCategory.INPUT_FORMS, 'iot-hub.category.input-forms'] |
|||
]); |
|||
|
|||
export enum DashboardCategory { |
|||
MONITORING = 'MONITORING', |
|||
ANALYTICS = 'ANALYTICS', |
|||
DEVICE_MANAGEMENT = 'DEVICE_MANAGEMENT', |
|||
USER_MANAGEMENT = 'USER_MANAGEMENT', |
|||
ASSET_MANAGEMENT = 'ASSET_MANAGEMENT', |
|||
SCADA = 'SCADA', |
|||
REPORTING = 'REPORTING', |
|||
OVERVIEW = 'OVERVIEW', |
|||
OPERATIONS = 'OPERATIONS' |
|||
} |
|||
|
|||
export const dashboardCategoryTranslations = new Map<DashboardCategory, string>([ |
|||
[DashboardCategory.MONITORING, 'iot-hub.category.monitoring'], |
|||
[DashboardCategory.ANALYTICS, 'iot-hub.category.analytics'], |
|||
[DashboardCategory.DEVICE_MANAGEMENT, 'iot-hub.category.device-management'], |
|||
[DashboardCategory.USER_MANAGEMENT, 'iot-hub.category.user-management'], |
|||
[DashboardCategory.ASSET_MANAGEMENT, 'iot-hub.category.asset-management'], |
|||
[DashboardCategory.SCADA, 'iot-hub.category.scada'], |
|||
[DashboardCategory.REPORTING, 'iot-hub.category.reporting'], |
|||
[DashboardCategory.OVERVIEW, 'iot-hub.category.overview'], |
|||
[DashboardCategory.OPERATIONS, 'iot-hub.category.operations'] |
|||
]); |
|||
|
|||
export enum CalcFieldCategory { |
|||
AGGREGATION = 'AGGREGATION', |
|||
GEOSPATIAL = 'GEOSPATIAL', |
|||
STATISTICAL = 'STATISTICAL', |
|||
ENERGY = 'ENERGY', |
|||
ENVIRONMENTAL = 'ENVIRONMENTAL', |
|||
PREDICTIVE = 'PREDICTIVE', |
|||
CUSTOM_FORMULA = 'CUSTOM_FORMULA' |
|||
} |
|||
|
|||
export const calcFieldCategoryTranslations = new Map<CalcFieldCategory, string>([ |
|||
[CalcFieldCategory.AGGREGATION, 'iot-hub.category.aggregation'], |
|||
[CalcFieldCategory.GEOSPATIAL, 'iot-hub.category.geospatial'], |
|||
[CalcFieldCategory.STATISTICAL, 'iot-hub.category.statistical'], |
|||
[CalcFieldCategory.ENERGY, 'iot-hub.category.energy'], |
|||
[CalcFieldCategory.ENVIRONMENTAL, 'iot-hub.category.environmental'], |
|||
[CalcFieldCategory.PREDICTIVE, 'iot-hub.category.predictive'], |
|||
[CalcFieldCategory.CUSTOM_FORMULA, 'iot-hub.category.custom-formula'] |
|||
]); |
|||
|
|||
export enum RuleChainCategory { |
|||
DATA_PROCESSING = 'DATA_PROCESSING', |
|||
ALERTING = 'ALERTING', |
|||
DEVICE_CONNECTIVITY = 'DEVICE_CONNECTIVITY', |
|||
INTEGRATION = 'INTEGRATION', |
|||
ANALYTICS = 'ANALYTICS' |
|||
} |
|||
|
|||
export const ruleChainCategoryTranslations = new Map<RuleChainCategory, string>([ |
|||
[RuleChainCategory.DATA_PROCESSING, 'iot-hub.category.data-processing'], |
|||
[RuleChainCategory.ALERTING, 'iot-hub.category.alerting'], |
|||
[RuleChainCategory.DEVICE_CONNECTIVITY, 'iot-hub.category.device-connectivity'], |
|||
[RuleChainCategory.INTEGRATION, 'iot-hub.category.integration'], |
|||
[RuleChainCategory.ANALYTICS, 'iot-hub.category.analytics'] |
|||
]); |
|||
|
|||
export enum DeviceCategory { |
|||
SENSORS = 'SENSORS', |
|||
GATEWAYS = 'GATEWAYS', |
|||
CONTROLLERS = 'CONTROLLERS', |
|||
ACTUATORS = 'ACTUATORS', |
|||
TRACKERS = 'TRACKERS', |
|||
METERS = 'METERS' |
|||
} |
|||
|
|||
export const deviceCategoryTranslations = new Map<DeviceCategory, string>([ |
|||
[DeviceCategory.SENSORS, 'iot-hub.category.sensors'], |
|||
[DeviceCategory.GATEWAYS, 'iot-hub.category.gateways'], |
|||
[DeviceCategory.CONTROLLERS, 'iot-hub.category.controllers'], |
|||
[DeviceCategory.ACTUATORS, 'iot-hub.category.actuators'], |
|||
[DeviceCategory.TRACKERS, 'iot-hub.category.trackers'], |
|||
[DeviceCategory.METERS, 'iot-hub.category.meters'] |
|||
]); |
|||
|
|||
export enum UseCase { |
|||
SMART_HOME = 'SMART_HOME', |
|||
INDUSTRIAL_IOT = 'INDUSTRIAL_IOT', |
|||
ENERGY_MANAGEMENT = 'ENERGY_MANAGEMENT', |
|||
FLEET_MANAGEMENT = 'FLEET_MANAGEMENT', |
|||
AGRICULTURE = 'AGRICULTURE', |
|||
SMART_CITY = 'SMART_CITY', |
|||
HEALTHCARE = 'HEALTHCARE', |
|||
RETAIL = 'RETAIL', |
|||
WATER_UTILITIES = 'WATER_UTILITIES' |
|||
} |
|||
|
|||
export const useCaseTranslations = new Map<UseCase, string>([ |
|||
[UseCase.SMART_HOME, 'iot-hub.use-case.smart-home'], |
|||
[UseCase.INDUSTRIAL_IOT, 'iot-hub.use-case.industrial-iot'], |
|||
[UseCase.ENERGY_MANAGEMENT, 'iot-hub.use-case.energy-management'], |
|||
[UseCase.FLEET_MANAGEMENT, 'iot-hub.use-case.fleet-management'], |
|||
[UseCase.AGRICULTURE, 'iot-hub.use-case.agriculture'], |
|||
[UseCase.SMART_CITY, 'iot-hub.use-case.smart-city'], |
|||
[UseCase.HEALTHCARE, 'iot-hub.use-case.healthcare'], |
|||
[UseCase.RETAIL, 'iot-hub.use-case.retail'], |
|||
[UseCase.WATER_UTILITIES, 'iot-hub.use-case.water-utilities'] |
|||
]); |
|||
|
|||
export function getCategoriesForType(type: ItemType): Map<string, string> { |
|||
switch (type) { |
|||
case ItemType.WIDGET: |
|||
return widgetCategoryTranslations as Map<string, string>; |
|||
case ItemType.DASHBOARD: |
|||
return dashboardCategoryTranslations as Map<string, string>; |
|||
case ItemType.CALCULATED_FIELD: |
|||
return calcFieldCategoryTranslations as Map<string, string>; |
|||
case ItemType.RULE_CHAIN: |
|||
return ruleChainCategoryTranslations as Map<string, string>; |
|||
case ItemType.DEVICE: |
|||
return deviceCategoryTranslations as Map<string, string>; |
|||
default: |
|||
return new Map(); |
|||
} |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
///
|
|||
/// 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 { ItemType } from './iot-hub-item.models'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
|
|||
export const widgetTypeTranslations = new Map<string, string>([ |
|||
['timeseries', 'item-data.widget-type-timeseries'], |
|||
['latest', 'item-data.widget-type-latest'], |
|||
['rpc', 'item-data.widget-type-rpc'], |
|||
['alarm', 'item-data.widget-type-alarm'], |
|||
['static', 'item-data.widget-type-static'], |
|||
]); |
|||
|
|||
export const cfTypeTranslations = new Map<string, string>([ |
|||
['SIMPLE', 'item-data.cf-type-simple'], |
|||
['SCRIPT', 'item-data.cf-type-script'], |
|||
['GEOFENCING', 'item-data.cf-type-geofencing'], |
|||
['ALARM', 'item-data.cf-type-alarm'], |
|||
['PROPAGATION', 'item-data.cf-type-propagation'], |
|||
['RELATED_ENTITIES_AGGREGATION', 'item-data.cf-type-related-entities-aggregation'], |
|||
['ENTITY_AGGREGATION', 'item-data.cf-type-entity-aggregation'], |
|||
]); |
|||
|
|||
export const cfTypeIcons = new Map<string, string>([ |
|||
['SIMPLE', 'calculate'], |
|||
['SCRIPT', 'code'], |
|||
['GEOFENCING', 'share_location'], |
|||
['ALARM', 'notification_important'], |
|||
['PROPAGATION', 'account_tree'], |
|||
['RELATED_ENTITIES_AGGREGATION', 'hub'], |
|||
['ENTITY_AGGREGATION', 'functions'], |
|||
]); |
|||
|
|||
export enum NodeComponentType { |
|||
ENRICHMENT = 'ENRICHMENT', |
|||
FILTER = 'FILTER', |
|||
TRANSFORMATION = 'TRANSFORMATION', |
|||
ACTION = 'ACTION', |
|||
ANALYTICS = 'ANALYTICS', |
|||
EXTERNAL = 'EXTERNAL', |
|||
FLOW = 'FLOW', |
|||
UNKNOWN = 'UNKNOWN' |
|||
} |
|||
|
|||
export const nodeComponentTypeTranslations = new Map<NodeComponentType, string>([ |
|||
[NodeComponentType.ENRICHMENT, 'item-data.node-type-enrichment'], |
|||
[NodeComponentType.FILTER, 'item-data.node-type-filter'], |
|||
[NodeComponentType.TRANSFORMATION, 'item-data.node-type-transformation'], |
|||
[NodeComponentType.ACTION, 'item-data.node-type-action'], |
|||
[NodeComponentType.ANALYTICS, 'item-data.node-type-analytics'], |
|||
[NodeComponentType.EXTERNAL, 'item-data.node-type-external'], |
|||
[NodeComponentType.FLOW, 'item-data.node-type-flow'], |
|||
[NodeComponentType.UNKNOWN, 'item-data.node-type-unknown'], |
|||
]); |
|||
|
|||
export interface NodeInfo { |
|||
name: string; |
|||
type: NodeComponentType; |
|||
} |
|||
|
|||
export const ruleChainTypeTranslations = new Map<string, string>([ |
|||
['CORE', 'item-data.rule-chain-type-core'], |
|||
['EDGE', 'item-data.rule-chain-type-edge'], |
|||
]); |
|||
|
|||
export interface MpItemVersionView { |
|||
id: string; |
|||
createdTime: number; |
|||
version: string; |
|||
publishedTime: number; |
|||
changelog: string; |
|||
dataDescriptor: any; |
|||
image: string; |
|||
icon: string; |
|||
color: string; |
|||
description: string; |
|||
categories: string[]; |
|||
useCases: string[]; |
|||
itemId: string; |
|||
creatorId: string; |
|||
name: string; |
|||
type: ItemType; |
|||
peOnly: boolean; |
|||
tags: string[]; |
|||
creatorDisplayName: string; |
|||
creatorWebsite: string; |
|||
creatorContactEmail: string; |
|||
creatorDescription: string; |
|||
creatorAvatarUrl: string; |
|||
installCount: number; |
|||
totalInstallCount: number; |
|||
} |
|||
|
|||
export class MpItemVersionQuery { |
|||
constructor( |
|||
public pageLink: PageLink, |
|||
public type?: string, |
|||
public peOnly?: boolean, |
|||
public creatorId?: string, |
|||
public categories?: string[], |
|||
public useCases?: string[], |
|||
public cfTypes?: string[], |
|||
public widgetTypes?: string[], |
|||
public ruleChainTypes?: string[], |
|||
public tbVersion?: number |
|||
) {} |
|||
|
|||
public toQuery(): string { |
|||
let query = this.pageLink.toQuery(); |
|||
if (this.type) { |
|||
query += `&type=${this.type}`; |
|||
} |
|||
if (this.peOnly != null) { |
|||
query += `&peOnly=${this.peOnly}`; |
|||
} |
|||
if (this.creatorId) { |
|||
query += `&creatorId=${this.creatorId}`; |
|||
} |
|||
if (this.categories?.length) { |
|||
query += this.categories.map(c => `&categories=${encodeURIComponent(c)}`).join(''); |
|||
} |
|||
if (this.useCases?.length) { |
|||
query += this.useCases.map(u => `&useCases=${encodeURIComponent(u)}`).join(''); |
|||
} |
|||
if (this.cfTypes?.length) { |
|||
query += this.cfTypes.map(t => `&cfTypes=${encodeURIComponent(t)}`).join(''); |
|||
} |
|||
if (this.widgetTypes?.length) { |
|||
query += this.widgetTypes.map(t => `&widgetTypes=${encodeURIComponent(t)}`).join(''); |
|||
} |
|||
if (this.ruleChainTypes?.length) { |
|||
query += this.ruleChainTypes.map(t => `&ruleChainTypes=${encodeURIComponent(t)}`).join(''); |
|||
} |
|||
if (this.tbVersion != null) { |
|||
query += `&tbVersion=${this.tbVersion}`; |
|||
} |
|||
return query; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue