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