Browse Source

feat(iot-hub): listing-slug deep links + edition / version gates

- 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.
pull/15772/head
Igor Kulikov 4 months ago
parent
commit
9c1e079685
  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. 40
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html
  4. 77
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss
  5. 47
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts
  6. 43
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html
  7. 76
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss
  8. 59
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts
  9. 52
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html
  10. 143
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss
  11. 43
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts
  12. 73
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts
  13. 12
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  14. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts
  15. 9
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts
  16. 15
      ui-ngx/src/assets/iot-hub/upgrade-required.svg
  17. 9
      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

@ -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 { }

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.alarm-rules-unavailable-upgrade' | 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.alarm-rules-unavailable-upgrade' | 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');
}
}

52
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html

@ -0,0 +1,52 @@
<!--
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-alarm-rules-unavailable">
<!-- Alarm-rules tinted glow blobs (same layout/colour rules as the
home page hero). -->
<div class="tb-iot-hub-blob tb-iot-hub-blob-left" [style.color]="alarmRulesColor"></div>
<div class="tb-iot-hub-blob tb-iot-hub-blob-right" [style.color]="alarmRulesColor"></div>
<div class="tb-alarm-rules-banner-container">
<div class="tb-alarm-rules-banner">
<img class="tb-alarm-rules-banner-image"
src="assets/iot-hub/items-page-alarm-rules-hero.svg" alt="">
<h1 class="tb-alarm-rules-banner-title">
{{ 'iot-hub.alarm-rules-unavailable-title-prefix' | translate }}
<span class="tb-alarm-rules-banner-version">{{ 'iot-hub.alarm-rules-unavailable-tb-version' | translate }}</span>
</h1>
<p class="tb-alarm-rules-banner-desc">
{{ 'iot-hub.alarm-rules-unavailable-desc' | translate }}
</p>
<div class="tb-alarm-rules-banner-actions">
<button mat-raised-button color="primary" (click)="upgradeInstance()">
{{ 'iot-hub.alarm-rules-unavailable-upgrade' | translate }}
</button>
<button mat-stroked-button (click)="goBack()">
{{ 'iot-hub.back-to-iot-hub' | translate }}
</button>
</div>
<div class="tb-alarm-rules-banner-current">
{{ 'iot-hub.alarm-rules-unavailable-current' | translate:{ version: currentTbVersion } }}
</div>
</div>
</div>
</div>

143
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;
}

43
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');
}
}

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) {

12
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,

4
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,

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

9
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 <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.",
"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