Browse Source

Merge with feature/iot-hub

pull/15540/head
Igor Kulikov 5 days ago
parent
commit
f68cc54ea9
  1. 33
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  2. 10
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
  3. 5
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  4. 40
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html
  5. 77
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss
  6. 47
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts
  7. 43
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html
  8. 76
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss
  9. 59
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts
  10. 73
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts
  11. 8
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  12. 9
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts
  13. 15
      ui-ngx/src/assets/iot-hub/upgrade-required.svg
  14. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json

33
ui-ngx/src/app/core/http/iot-hub-api.service.ts

@ -39,6 +39,16 @@ export function tbVersionToInt(version: string): number {
return major * 100 + minor * 10 + patch;
}
// Inverse of tbVersionToInt — produces "{major}.{minor}" by default
// and appends ".{patch}" when the patch component is non-zero so the
// rendered version stays compact (e.g. 430 → "4.3", 421 → "4.2.1").
export function tbVersionIntToString(version: number): string {
const major = Math.floor(version / 100);
const minor = Math.floor((version % 100) / 10);
const patch = version % 10;
return patch === 0 ? `${major}.${minor}` : `${major}.${minor}.${patch}`;
}
export function iotHubResourceUrl(baseUrl: string, path: string): string {
if (!path) {
return path;
@ -121,6 +131,29 @@ export class IotHubApiService {
);
}
/**
* Resolves the listing item-version that matches the caller's edition
* and platform version.
*
* Backend: GET /api/listings/public/by-slug/{slug}/item-version
*
* On success 200 with `MpItemVersionView`.
* On miss 404 with a `ListingItemVersionNotFound` body shape
* (`noMatchingVersions`, `peRequired`, or
* `minTbVersionRequired`). Callers should inspect
* `HttpErrorResponse.error` to differentiate.
*/
public getListingItemVersion(slug: string, config?: IotHubRequestConfig): Observable<MpItemVersionView> {
const queryParams = [
'ce=true',
`tbVersion=${tbVersionToInt(env.tbVersion)}`
];
return this.http.get<MpItemVersionView>(
`${this.baseUrl}/api/listings/public/by-slug/${encodeURIComponent(slug)}/item-version?${queryParams.join('&')}`,
{ 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),

10
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts

@ -32,6 +32,8 @@ import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-
import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component';
import { TbIotHubSelectCfEntityDialogComponent } from './iot-hub-select-cf-entity-dialog.component';
import { TbPeConnectivityMethodPromptComponent } from './pe-connectivity-method-prompt.component';
import { TbIotHubPeRequiredDialogComponent } from './iot-hub-pe-required-dialog.component';
import { TbIotHubUpgradeRequiredDialogComponent } from './iot-hub-upgrade-required-dialog.component';
import { TbIotHubMarkdownComponent } from './iot-hub-markdown.component';
import { SolutionInstallDialogComponent } from './solution-install-dialog.component';
import { IotHubActionsService } from './iot-hub-actions.service';
@ -55,7 +57,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent,
InstallFormRendererComponent
InstallFormRendererComponent,
TbIotHubPeRequiredDialogComponent,
TbIotHubUpgradeRequiredDialogComponent
],
imports: [
CommonModule,
@ -81,7 +85,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbIotHubSelectCfEntityDialogComponent,
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent
SolutionInstallDialogComponent,
TbIotHubPeRequiredDialogComponent,
TbIotHubUpgradeRequiredDialogComponent
]
})
export class IotHubComponentsModule { }

5
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts

@ -61,8 +61,9 @@ export class TbIotHubItemCardComponent {
if (!this.item.image) {
return null;
}
const url = this.item.image.split('?')[0];
const resolved = url.endsWith('/preview') ? this.item.image : `${url}/preview`;
const [path, query] = this.item.image.split('?');
const original = path.endsWith('/preview') ? path.slice(0, -'/preview'.length) : path;
const resolved = query ? `${original}?${query}` : original;
return this.iotHubApiService.resolveResourceUrl(resolved);
}

40
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html

@ -0,0 +1,40 @@
<!--
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-pe-required">
<button mat-icon-button class="tb-iot-hub-pe-required-close" (click)="close()" tabindex="-1">
<mat-icon>close</mat-icon>
</button>
<span class="tb-iot-hub-pe-required-icon" aria-hidden="true"></span>
<h2 class="tb-iot-hub-pe-required-title">
{{ 'iot-hub.pe-required-title' | translate }}
</h2>
<p class="tb-iot-hub-pe-required-message"
[innerHTML]="'iot-hub.pe-required-message' | translate"></p>
<div class="tb-iot-hub-pe-required-actions">
<button mat-button (click)="close()">
{{ 'action.close' | translate }}
</button>
<button mat-flat-button color="primary" (click)="upgradeInstance()">
{{ 'iot-hub.upgrade-instance' | translate }}
</button>
</div>
</div>

77
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
display: block;
width: 500px;
max-width: 100%;
}
.tb-iot-hub-pe-required {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
padding: 40px;
background: white;
border-radius: 8px;
}
.tb-iot-hub-pe-required-close {
position: absolute;
top: 8px;
right: 8px;
}
// SVG illustration applied as a mask so the silhouette is filled
// with $tb-primary-color. PE builds inherit their own primary value
// without re-exporting the asset.
.tb-iot-hub-pe-required-icon {
display: block;
width: 100px;
height: 100px;
background-color: $tb-primary-color;
-webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
}
.tb-iot-hub-pe-required-title {
font-size: 24px;
font-weight: 500;
line-height: 32px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
.tb-iot-hub-pe-required-message {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
.tb-iot-hub-pe-required-actions {
display: flex;
align-items: center;
gap: 8px;
}

47
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts

@ -0,0 +1,47 @@
///
/// 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 } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
@Component({
selector: 'tb-iot-hub-pe-required-dialog',
standalone: false,
templateUrl: './iot-hub-pe-required-dialog.component.html',
styleUrls: ['./iot-hub-pe-required-dialog.component.scss']
})
export class TbIotHubPeRequiredDialogComponent extends DialogComponent<TbIotHubPeRequiredDialogComponent> {
constructor(
protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubPeRequiredDialogComponent>
) {
super(store, router, dialogRef);
}
close(): void {
this.dialogRef.close();
}
upgradeInstance(): void {
window.open('https://thingsboard.io/docs/pe/installation/upgrade-from-ce/', '_blank');
}
}

43
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html

@ -0,0 +1,43 @@
<!--
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-upgrade-required">
<button mat-icon-button class="tb-iot-hub-upgrade-required-close" (click)="close()" tabindex="-1">
<mat-icon>close</mat-icon>
</button>
<span class="tb-iot-hub-upgrade-required-icon" aria-hidden="true"></span>
<h2 class="tb-iot-hub-upgrade-required-title">
{{ 'iot-hub.upgrade-required-title' | translate }}
</h2>
<p class="tb-iot-hub-upgrade-required-message"
[innerHTML]="'iot-hub.upgrade-required-message' | translate:{
minVersion: minVersion,
currentVersion: currentVersion
}"></p>
<div class="tb-iot-hub-upgrade-required-actions">
<button mat-button (click)="close()">
{{ 'action.close' | translate }}
</button>
<button mat-flat-button color="primary" (click)="upgradeInstance()">
{{ 'iot-hub.upgrade-instance' | translate }}
</button>
</div>
</div>

76
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
display: block;
width: 500px;
max-width: 100%;
}
.tb-iot-hub-upgrade-required {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
padding: 40px;
background: white;
border-radius: 8px;
}
.tb-iot-hub-upgrade-required-close {
position: absolute;
top: 8px;
right: 8px;
}
// Re-use the upgrade-required.svg silhouette as a CSS mask so the icon
// tints with $tb-primary-color (PE overrides apply automatically).
.tb-iot-hub-upgrade-required-icon {
display: block;
width: 100px;
height: 100px;
background-color: $tb-primary-color;
-webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
}
.tb-iot-hub-upgrade-required-title {
font-size: 24px;
font-weight: 500;
line-height: 32px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
.tb-iot-hub-upgrade-required-message {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
.tb-iot-hub-upgrade-required-actions {
display: flex;
align-items: center;
gap: 8px;
}

59
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts

@ -0,0 +1,59 @@
///
/// 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 { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { tbVersionIntToString } from '@core/http/iot-hub-api.service';
import { environment as env } from '@env/environment';
export interface IotHubUpgradeRequiredDialogData {
minTbVersion: number;
}
@Component({
selector: 'tb-iot-hub-upgrade-required-dialog',
standalone: false,
templateUrl: './iot-hub-upgrade-required-dialog.component.html',
styleUrls: ['./iot-hub-upgrade-required-dialog.component.scss']
})
export class TbIotHubUpgradeRequiredDialogComponent extends DialogComponent<TbIotHubUpgradeRequiredDialogComponent> {
readonly minVersion: string;
readonly currentVersion: string;
constructor(
protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubUpgradeRequiredDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: IotHubUpgradeRequiredDialogData
) {
super(store, router, dialogRef);
this.minVersion = `v${tbVersionIntToString(data.minTbVersion)}`;
this.currentVersion = `v${env.tbVersion}`;
}
close(): void {
this.dialogRef.close();
}
upgradeInstance(): void {
window.open('https://thingsboard.io/docs/installation/upgrade-instructions/', '_blank');
}
}

73
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts

@ -20,9 +20,10 @@ import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { AppState } from '@core/core.state';
import { Observable } from 'rxjs';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ListingItemVersionNotFound, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import {
DeepLinkOpenItem,
isPublished,
@ -33,6 +34,13 @@ import {
IotHubUnpublishedWarningDialogData,
TbIotHubUnpublishedWarningDialogComponent
} from '@home/components/iot-hub/iot-hub-unpublished-warning-dialog.component';
import {
TbIotHubPeRequiredDialogComponent
} from '@home/components/iot-hub/iot-hub-pe-required-dialog.component';
import {
IotHubUpgradeRequiredDialogData,
TbIotHubUpgradeRequiredDialogComponent
} from '@home/components/iot-hub/iot-hub-upgrade-required-dialog.component';
@Component({
selector: 'tb-iot-hub-item-resolver',
@ -52,23 +60,44 @@ export class TbIotHubItemResolverComponent implements OnInit {
ngOnInit(): void {
const params = this.route.snapshot.paramMap;
const slug = params.get('slug');
const itemVersionId = params.get('itemVersionId');
const itemId = params.get('itemId');
const byVersion = itemVersionId != null;
const id = byVersion ? itemVersionId : itemId;
const bySlug = slug != null;
const byVersion = !bySlug && itemVersionId != null;
if (!isUUID(id)) {
this.failTo('iot-hub.deep-link-invalid-id');
return;
let fetch$: Observable<MpItemVersionView>;
if (bySlug) {
fetch$ = this.iotHubApi.getListingItemVersion(slug, { ignoreErrors: true });
} else {
const id = byVersion ? itemVersionId : itemId;
if (!isUUID(id)) {
this.failTo('iot-hub.deep-link-invalid-id');
return;
}
fetch$ = byVersion
? this.iotHubApi.getVersionInfo(id, { ignoreErrors: true })
: this.iotHubApi.getPublishedVersion(id, { ignoreErrors: true });
}
const fetch$ = byVersion
? this.iotHubApi.getVersionInfo(id, { ignoreErrors: true })
: this.iotHubApi.getPublishedVersion(id, { ignoreErrors: true });
fetch$.subscribe({
next: v => this.handleResolved(v, byVersion),
error: err => {
if (bySlug && err?.status === 404) {
const body = (err?.error ?? {}) as ListingItemVersionNotFound;
if (body.peRequired) {
this.showPeRequired();
return;
}
if (typeof body.minTbVersionRequired === 'number') {
this.showMinTbVersionRequired(body.minTbVersionRequired);
return;
}
if (body.noMatchingVersions) {
this.failTo('iot-hub.deep-link-not-found');
return;
}
}
const key = err?.status === 404
? 'iot-hub.deep-link-not-found'
: 'iot-hub.deep-link-fetch-failed';
@ -77,6 +106,30 @@ export class TbIotHubItemResolverComponent implements OnInit {
});
}
private showPeRequired(): void {
this.router.navigate(['/iot-hub'], { replaceUrl: true }).then(() => {
this.dialog.open(TbIotHubPeRequiredDialogComponent, {
panelClass: ['tb-dialog'],
disableClose: true,
autoFocus: false
});
});
}
private showMinTbVersionRequired(minTbVersion: number): void {
this.router.navigate(['/iot-hub'], { replaceUrl: true }).then(() => {
this.dialog.open<TbIotHubUpgradeRequiredDialogComponent, IotHubUpgradeRequiredDialogData>(
TbIotHubUpgradeRequiredDialogComponent,
{
panelClass: ['tb-dialog'],
disableClose: true,
autoFocus: false,
data: { minTbVersion }
}
);
});
}
private handleResolved(version: MpItemVersionView, byVersion: boolean): void {
const segment = typeSegment(version.type);
if (!segment) {

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

@ -150,6 +150,14 @@ const routes: Routes = [
}
}
},
{
path: 'listing/:slug',
component: TbIotHubItemResolverComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.item-detail'
}
},
{
path: 'version/:itemVersionId',
component: TbIotHubItemResolverComponent,

9
ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts

@ -112,6 +112,15 @@ export interface MpItemVersionView {
resources: MpItemVersionResource[];
}
// 404 body shapes returned by the public listing item-version endpoint
// (GET /api/listings/public/by-slug/{slug}/item-version) when no
// version matches the caller's edition / platform combination.
export interface ListingItemVersionNotFound {
noMatchingVersions?: boolean;
peRequired?: boolean;
minTbVersionRequired?: number;
}
export interface MpItemVersionQueryOptions {
type?: string;
peOnly?: boolean;

15
ui-ngx/src/assets/iot-hub/upgrade-required.svg

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" fill="none">
<!-- Designed to be applied via CSS `mask-image`: opacities on each
shape encode the original design so any `background-color` (CE
primary, PE primary, etc.) retints the icon. -->
<g clip-path="url(#clip-upgrade-required)">
<circle cx="50" cy="50" r="50" fill="#000" opacity="0.06"/>
<path d="M66.6654 49.9993L63.7279 52.9368L52.082 41.3118V66.666H47.9154V41.3118L36.2904 52.9577L33.332 49.9993L49.9987 33.3327L66.6654 49.9993Z" fill="#000"/>
<circle cx="50" cy="50" r="49.0625" stroke="#000" stroke-width="1.875" stroke-dasharray="10 10" opacity="0.4"/>
</g>
<defs>
<clipPath id="clip-upgrade-required">
<rect width="100" height="100" fill="#fff"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 827 B

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

@ -4232,6 +4232,11 @@
"items-page-desc-devices": "Explore the device library to find pre-configured connectivity templates you can deploy in minutes to connect your hardware instantly.",
"device-library": "Device Library",
"most-popular-in-iot-hub": "Most popular in IoT Hub",
"pe-required-title": "Professional Edition required",
"pe-required-message": "This item is available only on ThingsBoard <b>Professional Edition</b>. You are currently running <b>Community Edition</b>. To install it, upgrade to Professional Edition.",
"upgrade-required-title": "ThingsBoard upgrade required",
"upgrade-required-message": "This item requires ThingsBoard <b>{{minVersion}}</b> or newer. You are currently running <b>{{currentVersion}}</b>. Please upgrade your instance to install it.",
"upgrade-instance": "Upgrade instance",
"become-creator-text": "Submit your templates to the ThingsBoard IoT Hub to get featured and showcase your solutions to our global community.",
"contribute": "Contribute",
"creator-profile": "Creator Profile",

Loading…
Cancel
Save