Browse Source

Merge pull request #4695 from vvlladd28/improvement/ota-updates/select

[WIP] UI: Refactoring OTA update table and add supported OTA package external URL
pull/4698/head
Igor Kulikov 5 years ago
committed by GitHub
parent
commit
f96ea353f6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  2. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  3. 58
      ui-ngx/src/app/core/http/device-profile.service.ts
  4. 6
      ui-ngx/src/app/core/http/ota-package.service.ts
  5. 28
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  6. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  7. 2
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  8. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  9. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts
  10. 13
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  11. 3
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  12. 61
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts
  13. 101
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html
  14. 46
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts
  15. 26
      ui-ngx/src/app/shared/components/button/copy-button.component.html
  16. 30
      ui-ngx/src/app/shared/components/button/copy-button.component.scss
  17. 95
      ui-ngx/src/app/shared/components/button/copy-button.component.ts
  18. 2
      ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts
  19. 1
      ui-ngx/src/app/shared/models/ota-package.models.ts
  20. 7
      ui-ngx/src/app/shared/shared.module.ts
  21. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

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

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

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

58
ui-ngx/src/app/core/http/device-profile.service.ts

@ -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));
}

6
ui-ngx/src/app/core/http/ota-package.service.ts

@ -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));
}
}

28
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -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;">

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -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>();

2
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -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;

2
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

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

2
ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts

@ -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);
}

13
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -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;

3
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -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;

61
ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts

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

101
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html

@ -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-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-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.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">

46
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts

@ -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);
}
}

26
ui-ngx/src/app/shared/components/button/copy-button.component.html

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

30
ui-ngx/src/app/shared/components/button/copy-button.component.scss

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

95
ui-ngx/src/app/shared/components/button/copy-button.component.ts

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

2
ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts

@ -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)
);
}

1
ui-ngx/src/app/shared/models/ota-package.models.ts

@ -87,6 +87,7 @@ export interface OtaPackageInfo extends BaseData<OtaPackageId> {
title?: string;
version?: string;
hasData?: boolean;
url?: string;
fileName: string;
checksum?: string;
checksumAlgorithm?: ChecksumAlgorithm;

7
ui-ngx/src/app/shared/shared.module.ts

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

14
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -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…
Cancel
Save