From de794f2a285c701cf95fb8cc58a05349c606fc0e Mon Sep 17 00:00:00 2001 From: dpinkevych Date: Thu, 21 May 2026 11:12:20 +0300 Subject: [PATCH 1/2] fix preview imgs in the solutions tab --- .../home/components/iot-hub/iot-hub-item-card.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index 49e3574188..38c1e26dcf 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/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); } From 9c1e07968572c2a7070aee9ccebad8b8ee50d73d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 26 May 2026 13:43:07 +0300 Subject: [PATCH 2/2] feat(iot-hub): listing-slug deep links + edition / version gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a public listing item-version endpoint to IotHubApiService (GET /api/listings/public/by-slug/{slug}/item-version?ce=true& tbVersion=N) returning MpItemVersionView, with the new ListingItemVersionNotFound shape capturing the documented 404 bodies (noMatchingVersions / peRequired / minTbVersionRequired). - Wire a tbVersionIntToString helper alongside the existing tbVersionToInt so the dialogs can render the resolved versions compactly (430 → "4.3", 421 → "4.2.1"). - Route /iot-hub/listing/:slug to TbIotHubItemResolverComponent. Resolver picks the slug path first, dispatches the 404 bodies to three handlers: noMatchingVersions reuses the existing deep-link-not-found notification, peRequired opens a new dialog, minTbVersionRequired opens another with the resolved min version. - New TbIotHubPeRequiredDialogComponent renders the "Professional Edition required" prompt and TbIotHubUpgradeRequiredDialogComponent renders the "ThingsBoard upgrade required" prompt. Both reuse a single upgrade-required.svg asset (Figma export) tinted via CSS mask + background-color so the PE accent inherits without touching the asset. The shared "Upgrade instance" button links to the right upgrade doc per dialog. - New TbIotHubAlarmRulesUnavailablePageComponent replaces TbIotHubItemsPageComponent on /iot-hub/alarm-rules. It mirrors the iot-hub-home glow-blob + dot-pattern background (alarm-rule accent), renders the alarm-rules hero, a 28px title with the primary "Thingsboard v4.3" span, the description, an Upgrade instance link (https://thingsboard.io/docs/installation/upgrade- instructions/), Back to IoT Hub, and the current platform version read from env.tbVersion. - Locale: pe-required-title / pe-required-message (bolded "Professional Edition" / "Community Edition"), upgrade-required-title / upgrade-required-message (bolded interpolations), and the alarm- rules-unavailable-* keys for the new page. --- .../src/app/core/http/iot-hub-api.service.ts | 33 ++++ .../iot-hub/iot-hub-components.module.ts | 10 +- .../iot-hub-pe-required-dialog.component.html | 40 +++++ .../iot-hub-pe-required-dialog.component.scss | 77 ++++++++++ .../iot-hub-pe-required-dialog.component.ts | 47 ++++++ ...hub-upgrade-required-dialog.component.html | 43 ++++++ ...hub-upgrade-required-dialog.component.scss | 76 ++++++++++ ...t-hub-upgrade-required-dialog.component.ts | 59 ++++++++ ...larm-rules-unavailable-page.component.html | 52 +++++++ ...larm-rules-unavailable-page.component.scss | 143 ++++++++++++++++++ ...-alarm-rules-unavailable-page.component.ts | 43 ++++++ .../iot-hub-item-resolver.component.ts | 73 +++++++-- .../pages/iot-hub/iot-hub-routing.module.ts | 12 +- .../home/pages/iot-hub/iot-hub.module.ts | 4 +- .../models/iot-hub/iot-hub-version.models.ts | 9 ++ .../src/assets/iot-hub/upgrade-required.svg | 15 ++ .../assets/locale/locale.constant-en_US.json | 9 ++ 17 files changed, 730 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts create mode 100644 ui-ngx/src/assets/iot-hub/upgrade-required.svg 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",