committed by
GitHub
28 changed files with 597 additions and 13 deletions
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.exception; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
||||
|
|
||||
|
@Schema |
||||
|
public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse { |
||||
|
|
||||
|
private final EntityType entityType; |
||||
|
|
||||
|
private final Long limit; |
||||
|
|
||||
|
protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) { |
||||
|
super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); |
||||
|
this.entityType = entityType; |
||||
|
this.limit = limit; |
||||
|
} |
||||
|
|
||||
|
public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) { |
||||
|
return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit); |
||||
|
} |
||||
|
|
||||
|
@Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY) |
||||
|
public EntityType getEntityType() { |
||||
|
return entityType; |
||||
|
} |
||||
|
|
||||
|
@Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY) |
||||
|
public Long getLimit() { |
||||
|
return limit; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.notification.info; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; |
||||
|
|
||||
|
@Data |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
@Builder |
||||
|
public class EntitiesLimitIncreaseRequestNotificationInfo implements NotificationInfo { |
||||
|
|
||||
|
private EntityType entityType; |
||||
|
private String userEmail; |
||||
|
private String increaseLimitActionLabel; |
||||
|
private String increaseLimitLink; |
||||
|
private String baseUrl; |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, String> getTemplateData() { |
||||
|
return mapOf( |
||||
|
"entityType", entityType.getNormalName(), |
||||
|
"userEmail", userEmail, |
||||
|
"increaseLimitActionLabel", increaseLimitActionLabel, |
||||
|
"increaseLimitLink", increaseLimitLink, |
||||
|
"baseUrl", baseUrl |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 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="entity-limit-exceeded-dialog"> |
||||
|
<button mat-icon-button class="close-btn" (click)="cancel()"> |
||||
|
<tb-icon>close</tb-icon> |
||||
|
</button> |
||||
|
<div class="entity-limit-exceeded-bg"></div> |
||||
|
<div class="entity-limit-exceeded-title">{{ 'entity.limit-reached' | translate }}</div> |
||||
|
<div class="entity-limit-exceeded-container"> |
||||
|
<div class="entity-limit-exceeded-text" [innerHTML]="limitReachedText | safe: 'html'"></div> |
||||
|
</div> |
||||
|
<div class="flex flex-row justify-center gap-2"> |
||||
|
<button mat-flat-button color="primary" (click)="requestLimitIncrease($event)"> |
||||
|
{{ 'entity.request-limit-increase' | translate }} |
||||
|
</button> |
||||
|
<button mat-button (click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="request-entity-limit-increase-sysadmin-prompt"> |
||||
|
{{ 'entity.request-sysadmin-text' | translate }} |
||||
|
<a href="" (click)="loginAsSysAdmin($event)"> {{ 'entity.login-here' | translate }} </a> |
||||
|
{{ 'entity.to-increase-limit' | translate }} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,97 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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'; |
||||
|
|
||||
|
.entity-limit-exceeded-dialog { |
||||
|
max-width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
@media #{$mat-gt-xs} { |
||||
|
max-width: 500px; |
||||
|
max-height: 80vh; |
||||
|
} |
||||
|
|
||||
|
position: relative; |
||||
|
padding: 40px 40px 24px 40px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 20px; |
||||
|
|
||||
|
.close-btn { |
||||
|
position: absolute; |
||||
|
top: 8px; |
||||
|
right: 8px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
|
||||
|
.entity-limit-exceeded-bg { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 140px; |
||||
|
&:before { |
||||
|
content: ""; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: $tb-primary-color; |
||||
|
mask-image: url(/assets/entity-limit-reached.svg); |
||||
|
-webkit-mask-image: url(/assets/entity-limit-reached.svg); |
||||
|
mask-repeat: no-repeat; |
||||
|
-webkit-mask-repeat: no-repeat; |
||||
|
mask-size: contain; |
||||
|
-webkit-mask-size: contain; |
||||
|
mask-position: center; |
||||
|
-webkit-mask-position: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.entity-limit-exceeded-title { |
||||
|
font-size: 24px; |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
line-height: 32px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.entity-limit-exceeded-container { |
||||
|
flex: 1; |
||||
|
overflow-y: auto; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
.entity-limit-exceeded-text { |
||||
|
font-size: 16px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 24px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.request-entity-limit-increase-sysadmin-prompt { |
||||
|
text-align: center; |
||||
|
font-size: 14px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 20px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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 { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import { Component, Inject, ViewEncapsulation } from '@angular/core'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { AuthService } from '@core/auth/auth.service'; |
||||
|
import { TenantService } from '@core/http/tenant.service'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { NotificationService } from '@core/http/notification.service'; |
||||
|
|
||||
|
export interface EntityLimitExceededDialogData { |
||||
|
entityType: EntityType; |
||||
|
limit: number; |
||||
|
} |
||||
|
|
||||
|
// @dynamic
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-limit-exceeded-dialog', |
||||
|
templateUrl: './entity-limit-exceeded-dialog.component.html', |
||||
|
styleUrls: ['./entity-limit-exceeded-dialog.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class EntityLimitExceededDialogComponent extends DialogComponent<EntityLimitExceededDialogComponent> { |
||||
|
|
||||
|
limitReachedText: string; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EntityLimitExceededDialogData, |
||||
|
public dialogRef: MatDialogRef<EntityLimitExceededDialogComponent>, |
||||
|
private authService: AuthService, |
||||
|
private dialogs: DialogService, |
||||
|
private translate: TranslateService, |
||||
|
private tenantService: TenantService, |
||||
|
private notificationService: NotificationService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
let entitiesText: string; |
||||
|
if (data.limit > 1) { |
||||
|
entitiesText = data.limit + ' ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).typePlural) as string).toLowerCase(); |
||||
|
} else { |
||||
|
entitiesText = '1 ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase(); |
||||
|
} |
||||
|
this.limitReachedText = this.translate.instant('entity.limit-reached-text', { entities: entitiesText, entity: (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase() }); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(); |
||||
|
} |
||||
|
|
||||
|
requestLimitIncrease($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.notificationService.sendEntitiesLimitIncreaseRequest(this.data.entityType).subscribe( |
||||
|
() => { |
||||
|
this.dialogRef.close(); |
||||
|
this.dialogs.alert( |
||||
|
this.translate.instant('entity.increase-limit-request-sent-title'), |
||||
|
this.translate.instant('entity.increase-limit-request-sent-text'), |
||||
|
this.translate.instant('action.close') |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
loginAsSysAdmin($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.preventDefault(); |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.tenantService.getTenant(getCurrentAuthUser(this.store).tenantId).subscribe( |
||||
|
(tenant) => { |
||||
|
this.authService.redirectUrl = `/tenantProfiles/${tenant.tenantProfileId.id}`; |
||||
|
this.authService.logout(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,46 @@ |
|||||
|
#### Entity count limit increase request notification templatization |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
Notification subject and message fields support templatization. |
||||
|
The list of available templatization parameters depends on the template type. |
||||
|
See the available types and parameters below: |
||||
|
|
||||
|
Available template parameters: |
||||
|
|
||||
|
* `entityType` - one of: 'Device', 'Asset', 'Customer', 'User', 'Dashboard', 'Rule chain', 'Edge'; |
||||
|
* `userEmail` - email of the user who sends the request; |
||||
|
* `increaseLimitActionLabel` - label of the button used to open Limits Management page, for ex: 'Set new limit'; |
||||
|
* `increaseLimitLink` - link to the Limits Management page; |
||||
|
* `baseUrl` - used to construct the full URL for the Limits Management page in email notifications; |
||||
|
|
||||
|
Parameter names must be wrapped using `${...}`. For example: `${userEmail}`. |
||||
|
You may also modify the value of the parameter with one of the suffixes: |
||||
|
|
||||
|
* `upperCase`, for example - `${userEmail:upperCase}` |
||||
|
* `lowerCase`, for example - `${userEmail:lowerCase}` |
||||
|
* `capitalize`, for example - `${userEmail:capitalize}` |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
Let's assume the notification about the increasing limit of the maximum allowed devices for the tenant. |
||||
|
The following template: |
||||
|
|
||||
|
```text |
||||
|
${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit. |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
will be transformed to: |
||||
|
|
||||
|
```text |
||||
|
johndoe@company.com has reached the maximum number of devices allowed and is requesting an increase to the device limit. |
||||
|
``` |
||||
|
|
||||
|
<br/> |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
Loading…
Reference in new issue