Merge pull request #4695 from vvlladd28/improvement/ota-updates/select
[WIP] UI: Refactoring OTA update table and add supported OTA package external URL
This commit is contained in:
commit
f96ea353f6
@ -434,7 +434,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
|
||||
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
||||
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
||||
}
|
||||
if (firmware.getData() == null) {
|
||||
if (firmware.getData() == null && !firmware.hasUrl()) {
|
||||
throw new DataValidationException("Can't assign firmware with empty data!");
|
||||
}
|
||||
if (!firmware.getDeviceProfileId().equals(deviceProfile.getId())) {
|
||||
|
||||
@ -716,7 +716,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
|
||||
if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) {
|
||||
throw new DataValidationException("Can't assign firmware with type: " + firmware.getType());
|
||||
}
|
||||
if (firmware.getData() == null) {
|
||||
if (firmware.getData() == null && !firmware.hasUrl()) {
|
||||
throw new DataValidationException("Can't assign firmware with empty data!");
|
||||
}
|
||||
if (!firmware.getDeviceProfileId().equals(device.getDeviceProfileId())) {
|
||||
|
||||
@ -14,16 +14,21 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {PageLink} from '@shared/models/page/page-link';
|
||||
import {defaultHttpOptionsFromConfig, RequestConfig} from './http-utils';
|
||||
import {Observable} from 'rxjs';
|
||||
import {PageData} from '@shared/models/page/page-data';
|
||||
import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models';
|
||||
import {isDefinedAndNotNull, isEmptyStr} from '@core/utils';
|
||||
import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
|
||||
import {SortOrder} from '@shared/models/page/sort-order';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PageLink } from '@shared/models/page/page-link';
|
||||
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
|
||||
import { forkJoin, Observable, of, throwError } from 'rxjs';
|
||||
import { PageData } from '@shared/models/page/page-data';
|
||||
import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
|
||||
import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
|
||||
import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
|
||||
import { SortOrder } from '@shared/models/page/sort-order';
|
||||
import { OtaPackageService } from '@core/http/ota-package.service';
|
||||
import { OtaUpdateType } from '@shared/models/ota-package.models';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -31,7 +36,10 @@ import {SortOrder} from '@shared/models/page/sort-order';
|
||||
export class DeviceProfileService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
private http: HttpClient,
|
||||
private otaPackageService: OtaPackageService,
|
||||
private dialogService: DialogService,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
}
|
||||
|
||||
@ -70,6 +78,34 @@ export class DeviceProfileService {
|
||||
);
|
||||
}
|
||||
|
||||
public saveDeviceProfileAndConfirmOtaChange(originDeviceProfile: DeviceProfile, deviceProfile: DeviceProfile,
|
||||
config?: RequestConfig): Observable<DeviceProfile> {
|
||||
const tasks: Observable<number>[] = [];
|
||||
if (originDeviceProfile?.id?.id && originDeviceProfile.firmwareId?.id !== deviceProfile.firmwareId?.id) {
|
||||
tasks.push(this.otaPackageService.countUpdateDeviceAfterChangePackage(OtaUpdateType.FIRMWARE, deviceProfile.id.id));
|
||||
} else {
|
||||
tasks.push(of(0));
|
||||
}
|
||||
if (originDeviceProfile?.id?.id && originDeviceProfile.softwareId?.id !== deviceProfile.softwareId?.id) {
|
||||
tasks.push(this.otaPackageService.countUpdateDeviceAfterChangePackage(OtaUpdateType.SOFTWARE, deviceProfile.id.id));
|
||||
} else {
|
||||
tasks.push(of(0));
|
||||
}
|
||||
return forkJoin(tasks).pipe(
|
||||
mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => {
|
||||
let text = '';
|
||||
if (deviceFirmwareUpdate > 0) {
|
||||
text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate});
|
||||
}
|
||||
if (deviceSoftwareUpdate > 0) {
|
||||
text += text.length ? ' ' : '';
|
||||
text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate});
|
||||
}
|
||||
return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true);
|
||||
}),
|
||||
mergeMap((update) => update ? this.saveDeviceProfile(deviceProfile, config) : throwError('Canceled saving device profiles')));
|
||||
}
|
||||
|
||||
public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable<DeviceProfile> {
|
||||
return this.http.post<DeviceProfile>('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export class OtaPackageService {
|
||||
}
|
||||
|
||||
public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType,
|
||||
hasData = true, config?: RequestConfig): Observable<PageData<OtaPackageInfo>> {
|
||||
config?: RequestConfig): Observable<PageData<OtaPackageInfo>> {
|
||||
const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`;
|
||||
return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
@ -120,4 +120,8 @@ export class OtaPackageService {
|
||||
return this.http.delete(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, deviceProfileId: string, config?: RequestConfig): Observable<number> {
|
||||
return this.http.get<number>(`/api/devices/count/${type}?deviceProfileId=${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -163,8 +163,34 @@
|
||||
*matCellDef="let entity; let row = index"
|
||||
[matTooltip]="cellTooltip(entity, column, row)"
|
||||
matTooltipPosition="above"
|
||||
[innerHTML]="cellContent(entity, column, row)"
|
||||
[ngStyle]="cellStyle(entity, column, row)">
|
||||
<span [innerHTML]="cellContent(entity, column, row)"></span>
|
||||
<ng-template [ngIf]="column.actionCell">
|
||||
<ng-container [ngSwitch]="column.actionCell.type">
|
||||
<ng-template [ngSwitchCase]="cellActionType.COPY_BUTTON">
|
||||
<tb-copy-button
|
||||
[disabled]="isLoading$ | async"
|
||||
[fxShow]="column.actionCell.isEnabled(entity)"
|
||||
[copyText]="column.actionCell.onAction(null, entity)"
|
||||
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
|
||||
tooltipPosition="above"
|
||||
[icon]="column.actionCell.icon"
|
||||
[mdiIcon]="column.actionCell.mdiIcon" [style]="column.actionCell.style">
|
||||
</tb-copy-button>
|
||||
</ng-template>
|
||||
<ng-template ngSwitchDefault>
|
||||
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||
[fxShow]="column.actionCell.isEnabled(entity)"
|
||||
matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
|
||||
matTooltipPosition="above"
|
||||
(click)="column.actionCell.onAction($event, entity)">
|
||||
<mat-icon [svgIcon]="column.actionCell.mdiIcon" [ngStyle]="column.actionCell.style">
|
||||
{{column.actionCell.icon}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;">
|
||||
|
||||
@ -43,7 +43,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { BaseData, HasId } from '@shared/models/base-data';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
CellActionDescriptor,
|
||||
CellActionDescriptor, CellActionDescriptorType,
|
||||
EntityActionTableColumn,
|
||||
EntityColumn,
|
||||
EntityTableColumn,
|
||||
@ -104,6 +104,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
|
||||
timewindow: Timewindow;
|
||||
dataSource: EntitiesDataSource<BaseData<HasId>>;
|
||||
|
||||
cellActionType = CellActionDescriptorType;
|
||||
|
||||
isDetailsOpen = false;
|
||||
detailsPanelOpened = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
|
||||
editingEntity.additionalInfo =
|
||||
mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo);
|
||||
}
|
||||
this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
|
||||
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe(
|
||||
(entity) => {
|
||||
this.entity = entity;
|
||||
this.entityComponent.entity = entity;
|
||||
|
||||
@ -66,5 +66,5 @@
|
||||
<mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')">
|
||||
{{ 'device-profile.device-profile-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-hint *ngIf="!!hint">{{ hint | translate }}</mat-hint>
|
||||
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
@ -90,7 +90,7 @@ export class DeviceProfileDialogComponent extends
|
||||
this.submitted = true;
|
||||
if (this.deviceProfileComponent.entityForm.valid) {
|
||||
this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()};
|
||||
this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe(
|
||||
this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(this.deviceProfile, this.deviceProfile).subscribe(
|
||||
(deviceProfile) => {
|
||||
this.dialogRef.close(deviceProfile);
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => str
|
||||
export type EntityVoidFunction<T extends BaseData<HasId>> = (entity: T) => void;
|
||||
export type EntityIdsVoidFunction<T extends BaseData<HasId>> = (ids: HasUUID[]) => void;
|
||||
export type EntityCountStringFunction = (count: number) => string;
|
||||
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>;
|
||||
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T, originalEntity?: T) => Observable<T>;
|
||||
export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>;
|
||||
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
|
||||
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
|
||||
@ -48,6 +48,9 @@ export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: st
|
||||
export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined;
|
||||
export type HeaderCellStyleFunction<T extends BaseData<HasId>> = (key: string) => object;
|
||||
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
|
||||
export type CopyCellContent<T extends BaseData<HasId>> = (entity: T, key: string, length: number) => object;
|
||||
|
||||
export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'}
|
||||
|
||||
export interface CellActionDescriptor<T extends BaseData<HasId>> {
|
||||
name: string;
|
||||
@ -56,7 +59,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> {
|
||||
mdiIcon?: string;
|
||||
style?: any;
|
||||
isEnabled: (entity: T) => boolean;
|
||||
onAction: ($event: MouseEvent, entity: T) => void;
|
||||
onAction: ($event: MouseEvent, entity: T) => any;
|
||||
type?: CellActionDescriptorType;
|
||||
}
|
||||
|
||||
export interface GroupActionDescriptor<T extends BaseData<HasId>> {
|
||||
@ -95,7 +99,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTabl
|
||||
public sortable: boolean = true,
|
||||
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
|
||||
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined,
|
||||
public isNumberColumn: boolean = false) {
|
||||
public isNumberColumn: boolean = false,
|
||||
public actionCell: CellActionDescriptor<T> = null) {
|
||||
super('content', key, title, width, sortable);
|
||||
}
|
||||
}
|
||||
@ -173,7 +178,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
|
||||
deleteEntitiesTitle: EntityCountStringFunction = () => '';
|
||||
deleteEntitiesContent: EntityCountStringFunction = () => '';
|
||||
loadEntity: EntityByIdOperation<T | L> = () => of();
|
||||
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
|
||||
saveEntity: EntityTwoWayOperation<T> = (entity, originalEntity) => of(entity);
|
||||
deleteEntity: EntityIdOneWayOperation = () => of();
|
||||
entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());
|
||||
onEntityAction: EntityActionFunction<T> = () => false;
|
||||
|
||||
@ -104,7 +104,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
|
||||
|
||||
this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink);
|
||||
this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id);
|
||||
this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile);
|
||||
this.config.saveEntity = (deviceProfile, originDeviceProfile) =>
|
||||
this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(originDeviceProfile, deviceProfile);
|
||||
this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id);
|
||||
this.config.onEntityAction = action => this.onDeviceProfileAction(action);
|
||||
this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Resolve } from '@angular/router';
|
||||
import {
|
||||
CellActionDescriptorType,
|
||||
DateEntityTableColumn,
|
||||
EntityTableColumn,
|
||||
EntityTableConfig
|
||||
@ -35,6 +36,8 @@ import { PageLink } from '@shared/models/page/page-link';
|
||||
import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
|
||||
import { EntityAction } from '@home/models/entity/entity-component.models';
|
||||
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
|
||||
@Injectable()
|
||||
export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<OtaPackage, PageLink, OtaPackageInfo>> {
|
||||
@ -44,6 +47,7 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
||||
|
||||
constructor(private translate: TranslateService,
|
||||
private datePipe: DatePipe,
|
||||
private store: Store<AppState>,
|
||||
private otaPackageService: OtaPackageService,
|
||||
private fileSize: FileSizePipe) {
|
||||
this.config.entityType = EntityType.OTA_PACKAGE;
|
||||
@ -55,25 +59,50 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
||||
|
||||
this.config.columns.push(
|
||||
new DateEntityTableColumn<OtaPackageInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
|
||||
new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '25%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '25%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '25%', entity => {
|
||||
new EntityTableColumn<OtaPackageInfo>('title', 'ota-update.title', '20%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('version', 'ota-update.version', '20%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('type', 'ota-update.package-type', '20%', entity => {
|
||||
return this.translate.instant(OtaUpdateTypeTranslationMap.get(entity.type));
|
||||
}),
|
||||
new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '25%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('url', 'ota-update.direct-url', '20%', entity => {
|
||||
return entity.url && entity.url.length > 20 ? `${entity.url.slice(0, 20)}…` : '';
|
||||
}, () => ({}), true, () => ({}), () => undefined, false,
|
||||
{
|
||||
name: this.translate.instant('ota-update.copy-direct-url'),
|
||||
icon: 'content_paste',
|
||||
style: {
|
||||
'font-size': '16px',
|
||||
color: 'rgba(0,0,0,.87)'
|
||||
},
|
||||
isEnabled: (otaPackage) => !!otaPackage.url,
|
||||
onAction: ($event, entity) => entity.url,
|
||||
type: CellActionDescriptorType.COPY_BUTTON
|
||||
}),
|
||||
new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '20%'),
|
||||
new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => {
|
||||
return this.fileSize.transform(entity.dataSize || 0);
|
||||
return entity.dataSize ? this.fileSize.transform(entity.dataSize) : '';
|
||||
}),
|
||||
new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '540px', entity => {
|
||||
return `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`;
|
||||
}, () => ({}), false)
|
||||
new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '220px', entity => {
|
||||
return entity.checksum ? this.checksumText(entity) : '';
|
||||
}, () => ({}), true, () => ({}), () => undefined, false,
|
||||
{
|
||||
name: this.translate.instant('ota-update.copy-checksum'),
|
||||
icon: 'content_paste',
|
||||
style: {
|
||||
'font-size': '16px',
|
||||
color: 'rgba(0,0,0,.87)'
|
||||
},
|
||||
isEnabled: (otaPackage) => !!otaPackage.checksum,
|
||||
onAction: ($event, entity) => entity.checksum,
|
||||
type: CellActionDescriptorType.COPY_BUTTON
|
||||
})
|
||||
);
|
||||
|
||||
this.config.cellActionDescriptors.push(
|
||||
{
|
||||
name: this.translate.instant('ota-update.download'),
|
||||
icon: 'file_download',
|
||||
isEnabled: (otaPackage) => otaPackage.hasData,
|
||||
isEnabled: (otaPackage) => otaPackage.hasData && !otaPackage.url,
|
||||
onAction: ($event, entity) => this.exportPackage($event, entity)
|
||||
}
|
||||
);
|
||||
@ -101,7 +130,19 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe();
|
||||
if (otaPackageInfo.url) {
|
||||
window.open(otaPackageInfo.url, '_blank');
|
||||
} else {
|
||||
this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
checksumText(entity): string {
|
||||
let text = `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`;
|
||||
if (text.length > 20) {
|
||||
text = `${text.slice(0, 20)}…`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
onPackageAction(action: EntityAction<OtaPackageInfo>): boolean {
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
-->
|
||||
<div class="tb-details-buttons" fxLayout.xs="column">
|
||||
<button mat-raised-button color="primary" fxFlex.xs
|
||||
[disabled]="(isLoading$ | async) || !entity?.hasData"
|
||||
[disabled]="(isLoading$ | async) || !(entity?.hasData && !entity?.url)"
|
||||
(click)="onEntityAction($event, 'uploadPackage')"
|
||||
[fxShow]="!isEdit">
|
||||
{{ 'ota-update.download' | translate }}
|
||||
@ -41,15 +41,23 @@
|
||||
ngxClipboard
|
||||
(cbOnSuccess)="onPackageChecksumCopied()"
|
||||
[cbContent]="entity?.checksum"
|
||||
[fxShow]="!isEdit">
|
||||
[fxShow]="!isEdit && entity?.checksum">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||
<span translate>ota-update.copy-checksum</span>
|
||||
</button>
|
||||
<button mat-raised-button
|
||||
ngxClipboard
|
||||
(cbOnSuccess)="onPackageDirectUrlCopied()"
|
||||
[cbContent]="entity?.url"
|
||||
[fxShow]="!isEdit && entity?.url">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||
<span translate>ota-update.copy-direct-url</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mat-padding" fxLayout="column" fxLayoutGap="8px">
|
||||
<form [formGroup]="entityForm">
|
||||
<fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column" fxLayoutGap="8px">
|
||||
<fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column">
|
||||
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
|
||||
<mat-form-field class="mat-block" fxFlex="45">
|
||||
<mat-label translate>ota-update.title</mat-label>
|
||||
@ -69,6 +77,7 @@
|
||||
<tb-device-profile-autocomplete
|
||||
formControlName="deviceProfileId"
|
||||
required
|
||||
[ngStyle]="{'padding-bottom': isAdd ? '16px': 0}"
|
||||
[hint]="'ota-update.chose-compatible-device-profile'"
|
||||
[editProfileEnabled]="false"
|
||||
[addNewProfile]="false"
|
||||
@ -82,44 +91,68 @@
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="mat-caption" translate *ngIf="isAdd">ota-update.warning-after-save-no-edit</div>
|
||||
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
||||
<mat-form-field class="mat-block" fxFlex="33">
|
||||
<mat-label translate>ota-update.checksum-algorithm</mat-label>
|
||||
<mat-select formControlName="checksumAlgorithm">
|
||||
<mat-option *ngFor="let checksumAlgorithm of checksumAlgorithms" [value]="checksumAlgorithm">
|
||||
{{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" fxFlex>
|
||||
<mat-label translate>ota-update.checksum</mat-label>
|
||||
<input matInput formControlName="checksum" type="text" [readonly]="!isAdd">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<section *ngIf="isAdd">
|
||||
<tb-file-input
|
||||
formControlName="file"
|
||||
workFromFileObj="true"
|
||||
required
|
||||
dropLabel="{{'ota-update.drop-file' | translate}}">
|
||||
</tb-file-input>
|
||||
<div class="mat-caption" style="margin: -8px 0 8px;" translate>ota-update.warning-after-save-no-edit</div>
|
||||
<mat-radio-group formControlName="resource" fxLayoutGap="16px">
|
||||
<mat-radio-button value="file">Upload binary file</mat-radio-button>
|
||||
<mat-radio-button value="url">Use external URL</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</section>
|
||||
<section *ngIf="!isAdd">
|
||||
<div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
||||
<section *ngIf="entityForm.get('resource').value === 'file'">
|
||||
<section *ngIf="isAdd">
|
||||
<tb-file-input
|
||||
formControlName="file"
|
||||
workFromFileObj="true"
|
||||
[required]="entityForm.get('resource').value === 'file'"
|
||||
dropLabel="{{'ota-update.drop-file' | translate}}">
|
||||
</tb-file-input>
|
||||
<mat-checkbox formControlName="generateChecksum" style="margin-top: 16px">
|
||||
{{ 'ota-update.auto-generate-checksum' | translate }}
|
||||
</mat-checkbox>
|
||||
</section>
|
||||
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px"
|
||||
fxLayout.xs="column" fxLayout.md="column" *ngIf="!(isAdd && this.entityForm.get('generateChecksum').value)">
|
||||
<mat-form-field class="mat-block" fxFlex="33">
|
||||
<mat-label translate>ota-update.file-name</mat-label>
|
||||
<input matInput formControlName="fileName" type="text" readonly>
|
||||
<mat-label translate>ota-update.checksum-algorithm</mat-label>
|
||||
<mat-select formControlName="checksumAlgorithm">
|
||||
<mat-option *ngFor="let checksumAlgorithm of checksumAlgorithms" [value]="checksumAlgorithm">
|
||||
{{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" fxFlex>
|
||||
<mat-label translate>ota-update.file-size-bytes</mat-label>
|
||||
<input matInput formControlName="dataSize" type="text" readonly>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" fxFlex>
|
||||
<mat-label translate>ota-update.content-type</mat-label>
|
||||
<input matInput formControlName="contentType" type="text" readonly>
|
||||
<mat-label translate>ota-update.checksum</mat-label>
|
||||
<input matInput formControlName="checksum" type="text">
|
||||
<mat-hint *ngIf="isAdd" translate>ota-update.checksum-hint</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<section *ngIf="!isAdd">
|
||||
<div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
|
||||
<mat-form-field class="mat-block" fxFlex="33">
|
||||
<mat-label translate>ota-update.file-name</mat-label>
|
||||
<input matInput formControlName="fileName" type="text">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" fxFlex>
|
||||
<mat-label translate>ota-update.file-size-bytes</mat-label>
|
||||
<input matInput formControlName="dataSize" type="text">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" fxFlex>
|
||||
<mat-label translate>ota-update.content-type</mat-label>
|
||||
<input matInput formControlName="contentType" type="text">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section *ngIf="entityForm.get('resource').value === 'url'" style="margin-top: 8px">
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>ota-update.direct-url</mat-label>
|
||||
<input matInput formControlName="url"
|
||||
type="text"
|
||||
[required]="entityForm.get('resource').value === 'url'">
|
||||
<mat-error *ngIf="entityForm.get('url').hasError('required')" translate>
|
||||
ota-update.direct-url-required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<div formGroupName="additionalInfo">
|
||||
<mat-form-field class="mat-block">
|
||||
|
||||
@ -30,6 +30,8 @@ import {
|
||||
OtaUpdateTypeTranslationMap
|
||||
} from '@shared/models/ota-package.models';
|
||||
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { isNotEmptyStr } from '@core/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-ota-update',
|
||||
@ -52,6 +54,26 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
super(store, fb, entityValue, entitiesTableConfigValue);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.entityForm.get('resource').valueChanges.pipe(
|
||||
filter(() => this.isAdd),
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe((resource) => {
|
||||
if (resource === 'file') {
|
||||
this.entityForm.get('url').clearValidators();
|
||||
this.entityForm.get('file').setValidators(Validators.required);
|
||||
this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
|
||||
this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
|
||||
} else {
|
||||
this.entityForm.get('file').clearValidators();
|
||||
this.entityForm.get('url').setValidators(Validators.required);
|
||||
this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
|
||||
this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
this.destroy$.next();
|
||||
@ -74,6 +96,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required],
|
||||
checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256],
|
||||
checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)],
|
||||
url: [entity ? entity.url : ''],
|
||||
resource: ['file'],
|
||||
additionalInfo: this.fb.group(
|
||||
{
|
||||
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
|
||||
@ -82,6 +106,7 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
});
|
||||
if (this.isAdd) {
|
||||
form.addControl('file', this.fb.control(null, Validators.required));
|
||||
form.addControl('generateChecksum', this.fb.control(true));
|
||||
} else {
|
||||
form.addControl('fileName', this.fb.control(null));
|
||||
form.addControl('dataSize', this.fb.control(null));
|
||||
@ -101,6 +126,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
fileName: entity.fileName,
|
||||
dataSize: entity.dataSize,
|
||||
contentType: entity.contentType,
|
||||
url: entity.url,
|
||||
resource: isNotEmptyStr(entity.url) ? 'url' : 'file',
|
||||
additionalInfo: {
|
||||
description: entity.additionalInfo ? entity.additionalInfo.description : ''
|
||||
}
|
||||
@ -108,8 +135,6 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
if (!this.isAdd && this.entityForm.enabled) {
|
||||
this.entityForm.disable({emitEvent: false});
|
||||
this.entityForm.get('additionalInfo').enable({emitEvent: false});
|
||||
// this.entityForm.get('dataSize').disable({emitEvent: false});
|
||||
// this.entityForm.get('contentType').disable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,4 +159,21 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
|
||||
horizontalPosition: 'right'
|
||||
}));
|
||||
}
|
||||
|
||||
onPackageDirectUrlCopied() {
|
||||
this.store.dispatch(new ActionNotificationShow(
|
||||
{
|
||||
message: this.translate.instant('ota-update.checksum-copied-message'),
|
||||
type: 'success',
|
||||
duration: 750,
|
||||
verticalPosition: 'bottom',
|
||||
horizontalPosition: 'right'
|
||||
}));
|
||||
}
|
||||
|
||||
prepareFormValue(formValue: any): any {
|
||||
delete formValue.resource;
|
||||
delete formValue.generateChecksum;
|
||||
return super.prepareFormValue(formValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2021 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.
|
||||
|
||||
-->
|
||||
<button mat-icon-button
|
||||
[disabled]="disabled"
|
||||
[matTooltip]="matTooltipText"
|
||||
[matTooltipPosition]="matTooltipPosition"
|
||||
(click)="copy($event)">
|
||||
<mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}">
|
||||
{{ iconSymbol }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright © 2016-2021 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.
|
||||
*/
|
||||
:host {
|
||||
.mat-icon-button{
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
line-height: 32px;
|
||||
.mat-icon.copied{
|
||||
color: #00C851 !important;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
.mat-icon{
|
||||
color: #28567E !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
///
|
||||
/// Copyright © 2016-2021 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 { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ClipboardService } from 'ngx-clipboard';
|
||||
import { TooltipPosition } from '@angular/material/tooltip';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-copy-button',
|
||||
styleUrls: ['copy-button.component.scss'],
|
||||
templateUrl: './copy-button.component.html'
|
||||
})
|
||||
export class CopyButtonComponent {
|
||||
|
||||
private copedIcon = '';
|
||||
private timer;
|
||||
|
||||
copied = false;
|
||||
|
||||
@Input()
|
||||
copyText: string;
|
||||
|
||||
@Input()
|
||||
disabled = false;
|
||||
|
||||
@Input()
|
||||
mdiIcon: string;
|
||||
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
tooltipText: string;
|
||||
|
||||
@Input()
|
||||
tooltipPosition: TooltipPosition;
|
||||
|
||||
@Input()
|
||||
style: {[key: string]: any} = {};
|
||||
|
||||
@Output()
|
||||
successCopied = new EventEmitter<string>();
|
||||
|
||||
constructor(private clipboardService: ClipboardService,
|
||||
private translate: TranslateService,
|
||||
private cd: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
copy($event: Event): void {
|
||||
$event.stopPropagation();
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.clipboardService.copy(this.copyText);
|
||||
this.successCopied.emit(this.copyText);
|
||||
this.copedIcon = 'done';
|
||||
this.copied = true;
|
||||
this.timer = setTimeout(() => {
|
||||
this.copedIcon = null;
|
||||
this.copied = false;
|
||||
this.cd.detectChanges();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
get iconSymbol(): string {
|
||||
return this.copedIcon || this.icon;
|
||||
}
|
||||
|
||||
get mdiIconSymbol(): string {
|
||||
return this.copedIcon ? '' : this.mdiIcon;
|
||||
}
|
||||
|
||||
get matTooltipText(): string {
|
||||
return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText;
|
||||
}
|
||||
|
||||
get matTooltipPosition(): TooltipPosition {
|
||||
return this.copied ? 'below' : this.tooltipPosition;
|
||||
}
|
||||
|
||||
}
|
||||
@ -216,7 +216,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
|
||||
direction: Direction.ASC
|
||||
});
|
||||
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
|
||||
true, {ignoreLoading: true}).pipe(
|
||||
{ignoreLoading: true}).pipe(
|
||||
map((data) => data && data.data.length ? data.data : null)
|
||||
);
|
||||
}
|
||||
|
||||
@ -87,6 +87,7 @@ export interface OtaPackageInfo extends BaseData<OtaPackageId> {
|
||||
title?: string;
|
||||
version?: string;
|
||||
hasData?: boolean;
|
||||
url?: string;
|
||||
fileName: string;
|
||||
checksum?: string;
|
||||
checksumAlgorithm?: ChecksumAlgorithm;
|
||||
|
||||
@ -143,6 +143,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
|
||||
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
|
||||
import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ota-package-autocomplete.component';
|
||||
import { MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
@ -240,7 +241,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
EntityGatewaySelectComponent,
|
||||
ContactComponent,
|
||||
OtaPackageAutocompleteComponent,
|
||||
WidgetsBundleSearchComponent
|
||||
WidgetsBundleSearchComponent,
|
||||
CopyButtonComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -412,7 +414,8 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
EntityGatewaySelectComponent,
|
||||
ContactComponent,
|
||||
OtaPackageAutocompleteComponent,
|
||||
WidgetsBundleSearchComponent
|
||||
WidgetsBundleSearchComponent,
|
||||
CopyButtonComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
||||
@ -575,7 +575,8 @@
|
||||
"enter-password": "Enter password",
|
||||
"enter-search": "Enter search",
|
||||
"created-time": "Created time",
|
||||
"loading": "Loading..."
|
||||
"loading": "Loading...",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"content-type": {
|
||||
"json": "Json",
|
||||
@ -2157,21 +2158,30 @@
|
||||
"assign-firmware-required": "Assigned firmware is required",
|
||||
"assign-software": "Assigned software",
|
||||
"assign-software-required": "Assigned software is required",
|
||||
"auto-generate-checksum": "Auto-generate checksum",
|
||||
"checksum": "Checksum",
|
||||
"checksum-hint": "If checksum is empty, it will be generated automatically",
|
||||
"checksum-algorithm": "Checksum algorithm",
|
||||
"checksum-copied-message": "Package checksum has been copied to clipboard",
|
||||
"chose-compatible-device-profile": "Choose compatible device profile",
|
||||
"change-firmware": "Change of the firmware may cause update of { count, plural, 1 {1 device} other {# devices} }.",
|
||||
"change-software": "Change of the software may cause update of { count, plural, 1 {1 device} other {# devices} }.",
|
||||
"chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.",
|
||||
"chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
|
||||
"chose-software-distributed-device": "Choose software that will be distributed to the devices",
|
||||
"content-type": "Content type",
|
||||
"copy-checksum": "Copy checksum",
|
||||
"copy-direct-url": "Copy direct URL",
|
||||
"copyId": "Copy package Id",
|
||||
"copied": "Copied!",
|
||||
"delete": "Delete package",
|
||||
"delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.",
|
||||
"delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?",
|
||||
"delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.",
|
||||
"delete-ota-updates-title": "Are you sure you want to delete { count, plural, 1 {1 OTA update} other {# OTA updates} }?",
|
||||
"description": "Description",
|
||||
"direct-url": "Direct URL",
|
||||
"direct-url-copied-message": "Package direct URL has been copied to clipboard",
|
||||
"direct-url-required": "Direct URL is required",
|
||||
"download": "Download package",
|
||||
"drop-file": "Drop a package file or click to select a file to upload.",
|
||||
"file-name": "File name",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user