diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index 12878ac0fc..b0f90ddeef 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/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 { + const queryParams = [ + 'ce=true', + `tbVersion=${tbVersionToInt(env.tbVersion)}` + ]; + return this.http.get( + `${this.baseUrl}/api/listings/public/by-slug/${encodeURIComponent(slug)}/item-version?${queryParams.join('&')}`, + { params: this.buildParams(config) } + ); + } + public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable { return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { params: this.buildParams(config), diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts index cb37201432..2df12eedce 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts @@ -31,6 +31,8 @@ import { TbIotHubSearchComponent } from './iot-hub-search.component'; import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-table.component'; import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-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'; @@ -53,7 +55,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link TbPeConnectivityMethodPromptComponent, TbIotHubMarkdownComponent, SolutionInstallDialogComponent, - InstallFormRendererComponent + InstallFormRendererComponent, + TbIotHubPeRequiredDialogComponent, + TbIotHubUpgradeRequiredDialogComponent ], imports: [ CommonModule, @@ -78,7 +82,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link TbIotHubInstalledItemsDialogComponent, TbPeConnectivityMethodPromptComponent, TbIotHubMarkdownComponent, - SolutionInstallDialogComponent + SolutionInstallDialogComponent, + TbIotHubPeRequiredDialogComponent, + TbIotHubUpgradeRequiredDialogComponent ] }) export class IotHubComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html new file mode 100644 index 0000000000..57e98e4cbb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html @@ -0,0 +1,40 @@ + +
+ + + + +

+ {{ 'iot-hub.pe-required-title' | translate }} +

+ +

+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss new file mode 100644 index 0000000000..8e5f0ad2e6 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts new file mode 100644 index 0000000000..640c8fad20 --- /dev/null +++ b/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 { + + constructor( + protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef + ) { + super(store, router, dialogRef); + } + + close(): void { + this.dialogRef.close(); + } + + upgradeInstance(): void { + window.open('https://thingsboard.io/docs/pe/installation/upgrade-from-ce/', '_blank'); + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html new file mode 100644 index 0000000000..e1fce18133 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html @@ -0,0 +1,43 @@ + +
+ + + + +

+ {{ 'iot-hub.upgrade-required-title' | translate }} +

+ +

+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss new file mode 100644 index 0000000000..3a4d7f8c08 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts new file mode 100644 index 0000000000..480d3109ba --- /dev/null +++ b/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 { + + readonly minVersion: string; + readonly currentVersion: string; + + constructor( + protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @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'); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html new file mode 100644 index 0000000000..18a47441ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html @@ -0,0 +1,52 @@ + +
+ +
+
+ +
+
+ + +

+ {{ 'iot-hub.alarm-rules-unavailable-title-prefix' | translate }} + {{ 'iot-hub.alarm-rules-unavailable-tb-version' | translate }} +

+ +

+ {{ 'iot-hub.alarm-rules-unavailable-desc' | translate }} +

+ +
+ + +
+ +
+ {{ 'iot-hub.alarm-rules-unavailable-current' | translate:{ version: currentTbVersion } }} +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss new file mode 100644 index 0000000000..d958011810 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss @@ -0,0 +1,143 @@ +/** + * 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"; + +// Outer page — 16px padding around the white container, matches +// iot-hub-home. +:host { + display: block; + padding: 16px; + height: 100%; +} + +// Page container — same white card / dot pattern / blob layering +// rules as the iot-hub home page. +.tb-iot-hub-alarm-rules-unavailable { + position: relative; + background: white; + border-radius: 8px; + overflow: hidden; + height: 100%; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 800px; + background-image: radial-gradient(circle, rgba(0, 0, 0, 0.1) 1px, transparent 1px); + background-size: 20px 20px; + mask-image: linear-gradient(to bottom, black 0%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + > *:not(.tb-iot-hub-blob) { + position: relative; + z-index: 2; + } +} + +// Soft colored glow blobs — copied verbatim from iot-hub-home so the +// page background matches. `color` is bound from the ALARM_RULE +// HeroTypeConfig entry. +.tb-iot-hub-blob { + position: absolute; + width: 568px; + height: 568px; + border-radius: 50%; + background: currentColor; + opacity: 0.16; + filter: blur(120px); + pointer-events: none; + z-index: 0; +} + +.tb-iot-hub-blob-left { + top: 56px; + left: -221px; +} + +.tb-iot-hub-blob-right { + top: 259px; + right: -196px; +} + +.tb-alarm-rules-banner-container { + height: 100%; + display: flex; + overflow-y: auto; + flex-direction: column; +} + +// Banner content stack — vertically centred in the card. +.tb-alarm-rules-banner { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 40px; + padding: 80px 40px; + margin: auto; +} + +.tb-alarm-rules-banner-image { + width: 446px; + max-width: 100%; + height: auto; +} + +.tb-alarm-rules-banner-title { + font-size: 28px; + font-weight: 500; + line-height: 36px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + margin: 0; + max-width: 900px; +} + +.tb-alarm-rules-banner-version { + color: $tb-primary-color; +} + +.tb-alarm-rules-banner-desc { + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + margin: -24px 0 0; + max-width: 900px; +} + +.tb-alarm-rules-banner-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.tb-alarm-rules-banner-current { + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 0, 0, 0.54); + margin-top: -28px; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts new file mode 100644 index 0000000000..c44a46ce0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts @@ -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. +/// + +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { environment as env } from '@env/environment'; + +@Component({ + selector: 'tb-iot-hub-alarm-rules-unavailable-page', + standalone: false, + templateUrl: './iot-hub-alarm-rules-unavailable-page.component.html', + styleUrls: ['./iot-hub-alarm-rules-unavailable-page.component.scss'] +}) +export class TbIotHubAlarmRulesUnavailablePageComponent { + + // Same colour as the ALARM_RULE HeroTypeConfig entry on the home page. + readonly alarmRulesColor = '#d66f2e'; + + readonly currentTbVersion: string = env.tbVersion; + + constructor(private router: Router) {} + + goBack(): void { + void this.router.navigate(['/iot-hub']); + } + + upgradeInstance(): void { + window.open('https://thingsboard.io/docs/installation/upgrade-instructions/', '_blank'); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts index 0239966f10..606d6a29ae 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts +++ b/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; + 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, + { + panelClass: ['tb-dialog'], + disableClose: true, + autoFocus: false, + data: { minTbVersion } + } + ); + }); + } + private handleResolved(version: MpItemVersionView, byVersion: boolean): void { const segment = typeSegment(version.type); if (!segment) { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts index 4a6cf80c52..3e387e6c0a 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts @@ -20,6 +20,7 @@ import { RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; +import { TbIotHubAlarmRulesUnavailablePageComponent } from './iot-hub-alarm-rules-unavailable-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; @@ -86,11 +87,10 @@ const routes: Routes = [ }, { path: 'alarm-rules', - component: TbIotHubItemsPageComponent, + component: TbIotHubAlarmRulesUnavailablePageComponent, data: { auth: [Authority.TENANT_ADMIN], title: 'item.type-alarm-rule-plural', - itemType: 'ALARM_RULE', breadcrumb: { label: 'item.type-alarm-rule-plural', icon: 'notification_important' } } }, @@ -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, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts index b586fc613c..23c4687ac0 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts @@ -21,6 +21,7 @@ import { IotHubComponentsModule } from '@home/components/iot-hub/iot-hub-compone import { IotHubRoutingModule } from './iot-hub-routing.module'; import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; +import { TbIotHubAlarmRulesUnavailablePageComponent } from './iot-hub-alarm-rules-unavailable-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; @@ -33,7 +34,8 @@ import { TbIotHubItemResolverComponent } from './iot-hub-item-resolver.component TbIotHubCreatorProfileComponent, TbIotHubInstalledItemsComponent, TbIotHubSearchPageComponent, - TbIotHubItemResolverComponent + TbIotHubItemResolverComponent, + TbIotHubAlarmRulesUnavailablePageComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts index 94bb971323..bd6f29cdb4 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts +++ b/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; diff --git a/ui-ngx/src/assets/iot-hub/upgrade-required.svg b/ui-ngx/src/assets/iot-hub/upgrade-required.svg new file mode 100644 index 0000000000..1c0973a83a --- /dev/null +++ b/ui-ngx/src/assets/iot-hub/upgrade-required.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e2cf1fd6e7..15ded4b87d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3742,6 +3742,15 @@ "most-popular-in-iot-hub": "Most popular in IoT Hub", "alarm-rule-install-update-required": "Update required", "alarm-rule-install-update-required-text": "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.", + "alarm-rules-unavailable-title-prefix": "Alarm Rules are available starting from", + "alarm-rules-unavailable-tb-version": "Thingsboard v4.3", + "alarm-rules-unavailable-desc": "Use pre-built Alarm Rule templates to detect critical conditions like low battery, threshold breaches, or devices going offline. Skip writing the rule logic and start reacting to incidents the moment they happen.", + "alarm-rules-unavailable-upgrade": "Upgrade instance", + "alarm-rules-unavailable-current": "Current platform version: ThingsBoard v{{version}}", + "pe-required-title": "Professional Edition required", + "pe-required-message": "This item is available only on ThingsBoard Professional Edition. You are currently running Community Edition. To install it, upgrade to Professional Edition.", + "upgrade-required-title": "ThingsBoard upgrade required", + "upgrade-required-message": "This item requires ThingsBoard {{minVersion}} or newer. You are currently running {{currentVersion}}. Please upgrade your instance to install it.", "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",