Browse Source
- Refactor update/delete endpoints to use installed item's own ID instead of marketplace itemId - Add SHA-256 checksum comparison to detect local entity modifications before update - Support force update to skip checksum check when user confirms overwrite - Add per-type update methods (widget, dashboard, calculated field, rule chain) - Add entityId field to CalculatedFieldInstalledItemDescriptor - Replace checkForUpdates with getItemsPublishedVersions API - Replace getInstalledItemInfos with getInstalledItemIds returning List<UUID> - Remove unused IotHubInstalledItemInfo class and resolver - Set metadata version from saved rule chain in install/update flows - Add entity-modified confirmation dialog in update UIpull/15347/head
33 changed files with 674 additions and 389 deletions
@ -1,32 +0,0 @@ |
|||||
/** |
|
||||
* 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. |
|
||||
*/ |
|
||||
package org.thingsboard.server.common.data.iot_hub; |
|
||||
|
|
||||
import lombok.AllArgsConstructor; |
|
||||
import lombok.Data; |
|
||||
import lombok.NoArgsConstructor; |
|
||||
|
|
||||
import java.util.UUID; |
|
||||
|
|
||||
@Data |
|
||||
@AllArgsConstructor |
|
||||
@NoArgsConstructor |
|
||||
public class IotHubInstalledItemInfo { |
|
||||
|
|
||||
private UUID itemId; |
|
||||
private UUID itemVersionId; |
|
||||
|
|
||||
} |
|
||||
@ -1,25 +0,0 @@ |
|||||
///
|
|
||||
/// 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 { inject } from '@angular/core'; |
|
||||
import { ResolveFn } from '@angular/router'; |
|
||||
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
|
||||
import { IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; |
|
||||
|
|
||||
export const iotHubInstalledItemInfosResolver: ResolveFn<IotHubInstalledItemInfo[]> = () => { |
|
||||
const iotHubApiService = inject(IotHubApiService); |
|
||||
return iotHubApiService.getInstalledItemInfos({ ignoreLoading: true }); |
|
||||
}; |
|
||||
@ -0,0 +1,230 @@ |
|||||
|
///
|
||||
|
/// 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 { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
||||
|
import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; |
||||
|
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { getEntityDetailsPageURL } from '@core/utils'; |
||||
|
|
||||
|
export interface IotHubUpdateDialogData { |
||||
|
installedItemId: string; |
||||
|
itemName: string; |
||||
|
itemType: ItemType; |
||||
|
version: string; |
||||
|
versionId: string; |
||||
|
iotHubApiService: IotHubApiService; |
||||
|
} |
||||
|
|
||||
|
export type UpdateState = 'confirm' | 'updating' | 'success' | 'error'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-iot-hub-update-dialog', |
||||
|
standalone: false, |
||||
|
template: ` |
||||
|
@switch (state) { |
||||
|
@case ('confirm') { |
||||
|
<h2 mat-dialog-title>{{ 'iot-hub.update-item-title' | translate }}</h2> |
||||
|
<mat-dialog-content> |
||||
|
<p>{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}</p> |
||||
|
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p> |
||||
|
</mat-dialog-content> |
||||
|
<mat-dialog-actions align="end"> |
||||
|
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button> |
||||
|
<button mat-raised-button color="primary" (click)="update()">{{ 'iot-hub.update' | translate }}</button> |
||||
|
</mat-dialog-actions> |
||||
|
} |
||||
|
@case ('updating') { |
||||
|
<h2 mat-dialog-title>{{ 'iot-hub.update-item-title' | translate }}</h2> |
||||
|
<mat-dialog-content> |
||||
|
<p>{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}</p> |
||||
|
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p> |
||||
|
</mat-dialog-content> |
||||
|
<mat-dialog-actions align="end"> |
||||
|
<button mat-button disabled>{{ 'action.cancel' | translate }}</button> |
||||
|
<button mat-raised-button color="primary" disabled> |
||||
|
<mat-spinner diameter="18" class="tb-iot-hub-inline-spinner"></mat-spinner> |
||||
|
{{ 'iot-hub.updating' | translate }} |
||||
|
</button> |
||||
|
</mat-dialog-actions> |
||||
|
} |
||||
|
@case ('success') { |
||||
|
<h2 mat-dialog-title> |
||||
|
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-success-icon">check_circle</mat-icon> |
||||
|
{{ 'iot-hub.update-success-title' | translate }} |
||||
|
</h2> |
||||
|
<mat-dialog-content> |
||||
|
<p>{{ 'iot-hub.update-success-message' | translate:{ name: data.itemName, version: data.version } }}</p> |
||||
|
</mat-dialog-content> |
||||
|
<mat-dialog-actions align="end"> |
||||
|
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button> |
||||
|
@if (entityDetailsUrl) { |
||||
|
<button mat-raised-button color="primary" (click)="openEntityDetails()"> |
||||
|
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }} |
||||
|
</button> |
||||
|
} |
||||
|
</mat-dialog-actions> |
||||
|
} |
||||
|
@case ('error') { |
||||
|
<h2 mat-dialog-title> |
||||
|
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-error-icon">error</mat-icon> |
||||
|
{{ 'iot-hub.update-error-title' | translate }} |
||||
|
</h2> |
||||
|
<mat-dialog-content> |
||||
|
<p>{{ 'iot-hub.update-error-message' | translate:{ name: data.itemName } }}</p> |
||||
|
<div class="tb-iot-hub-error-details">{{ errorMessage }}</div> |
||||
|
</mat-dialog-content> |
||||
|
<mat-dialog-actions align="end"> |
||||
|
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button> |
||||
|
</mat-dialog-actions> |
||||
|
} |
||||
|
} |
||||
|
`,
|
||||
|
styles: [` |
||||
|
.tb-iot-hub-install-meta { |
||||
|
margin: 4px 0; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
.tb-iot-hub-inline-spinner { |
||||
|
display: inline-block; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
.tb-iot-hub-result-icon { |
||||
|
vertical-align: middle; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
.tb-iot-hub-success-icon { |
||||
|
color: #2e7d32; |
||||
|
} |
||||
|
.tb-iot-hub-error-icon { |
||||
|
color: #c62828; |
||||
|
} |
||||
|
.tb-iot-hub-error-details { |
||||
|
max-height: 200px; |
||||
|
overflow-y: auto; |
||||
|
padding: 12px; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 4px; |
||||
|
font-family: monospace; |
||||
|
font-size: 12px; |
||||
|
color: rgba(0, 0, 0, 0.7); |
||||
|
white-space: pre-wrap; |
||||
|
word-break: break-word; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
`]
|
||||
|
}) |
||||
|
export class TbIotHubUpdateDialogComponent { |
||||
|
|
||||
|
private static readonly ITEM_TYPE_TO_ENTITY_TYPE: Record<string, EntityType> = { |
||||
|
'WIDGET': EntityType.WIDGET_TYPE, |
||||
|
'DASHBOARD': EntityType.DASHBOARD, |
||||
|
'CALCULATED_FIELD': EntityType.CALCULATED_FIELD, |
||||
|
'RULE_CHAIN': EntityType.RULE_CHAIN, |
||||
|
'DEVICE': EntityType.DEVICE_PROFILE |
||||
|
}; |
||||
|
|
||||
|
typeTranslations = itemTypeTranslations; |
||||
|
state: UpdateState = 'confirm'; |
||||
|
errorMessage = ''; |
||||
|
entityDetailsUrl: string | null = null; |
||||
|
|
||||
|
constructor( |
||||
|
@Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData, |
||||
|
private dialogRef: MatDialogRef<TbIotHubUpdateDialogComponent>, |
||||
|
private dialogService: DialogService, |
||||
|
private router: Router, |
||||
|
private translate: TranslateService |
||||
|
) {} |
||||
|
|
||||
|
getTypeLabel(): string { |
||||
|
const key = this.typeTranslations.get(this.data.itemType); |
||||
|
return key ? this.translate.instant(key) : ''; |
||||
|
} |
||||
|
|
||||
|
update(force = false): void { |
||||
|
this.state = 'updating'; |
||||
|
this.data.iotHubApiService.updateItemVersion(this.data.installedItemId, this.data.versionId, { ignoreLoading: true }, force).subscribe({ |
||||
|
next: (result) => { |
||||
|
if (result.success) { |
||||
|
this.state = 'success'; |
||||
|
this.entityDetailsUrl = this.resolveEntityDetailsUrl(result.descriptor); |
||||
|
} else if (result.entityModified) { |
||||
|
this.state = 'confirm'; |
||||
|
this.dialogService.confirm( |
||||
|
this.translate.instant('iot-hub.entity-modified-title'), |
||||
|
this.translate.instant('iot-hub.entity-modified-text', { type: this.getTypeLabel() }), |
||||
|
this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes') |
||||
|
).subscribe(confirmed => { |
||||
|
if (confirmed) { |
||||
|
this.update(true); |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
this.state = 'error'; |
||||
|
this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.update-error', { name: this.data.itemName }); |
||||
|
} |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.state = 'error'; |
||||
|
this.errorMessage = err?.error?.message || err?.message || this.translate.instant('iot-hub.update-error', { name: this.data.itemName }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
openEntityDetails(): void { |
||||
|
if (this.entityDetailsUrl) { |
||||
|
this.dialogRef.close('updated'); |
||||
|
this.router.navigateByUrl(this.entityDetailsUrl); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(this.state === 'success' ? 'updated' : false); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(false); |
||||
|
} |
||||
|
|
||||
|
private resolveEntityDetailsUrl(descriptor: IotHubInstalledItemDescriptor): string | null { |
||||
|
if (!descriptor) { |
||||
|
return null; |
||||
|
} |
||||
|
const entityType = TbIotHubUpdateDialogComponent.ITEM_TYPE_TO_ENTITY_TYPE[this.data.itemType]; |
||||
|
if (!entityType) { |
||||
|
return null; |
||||
|
} |
||||
|
let entityId: string | null = null; |
||||
|
switch (descriptor.type) { |
||||
|
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break; |
||||
|
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break; |
||||
|
case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break; |
||||
|
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break; |
||||
|
} |
||||
|
if (!entityId) { |
||||
|
return null; |
||||
|
} |
||||
|
return getEntityDetailsPageURL(entityId, entityType) || null; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue