Browse Source

UI: Added firmware configuration

pull/4380/head
Vladyslav_Prykhodko 6 years ago
parent
commit
45fc32ce3e
  1. 13
      application/src/main/java/org/thingsboard/server/controller/FirmwareController.java
  2. 9
      dao/src/main/java/org/thingsboard/server/dao/firmware/BaseFirmwareService.java
  3. 123
      ui-ngx/src/app/core/http/firmware.service.ts
  4. 8
      ui-ngx/src/app/core/http/http-utils.ts
  5. 12
      ui-ngx/src/app/core/services/menu.service.ts
  6. 2
      ui-ngx/src/app/core/utils.ts
  7. 2
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  8. 48
      ui-ngx/src/app/modules/home/pages/firmware/firmware-routing.module.ts
  9. 99
      ui-ngx/src/app/modules/home/pages/firmware/firmware-table-config.resolve.ts
  10. 35
      ui-ngx/src/app/modules/home/pages/firmware/firmware.module.ts
  11. 87
      ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html
  12. 112
      ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts
  13. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  14. 55
      ui-ngx/src/app/shared/components/file-input.component.ts
  15. 19
      ui-ngx/src/app/shared/models/entity-type.models.ts
  16. 50
      ui-ngx/src/app/shared/models/firmware.models.ts
  17. 26
      ui-ngx/src/app/shared/models/id/firmware-id.ts
  18. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json

13
application/src/main/java/org/thingsboard/server/controller/FirmwareController.java

@ -112,11 +112,10 @@ public class FirmwareController extends BaseController {
@RequestMapping(value = "/firmware/{firmwareId}", method = RequestMethod.POST)
@ResponseBody
public Firmware saveFirmwareData(@PathVariable(FIRMWARE_ID) String strFirmwareId,
@RequestParam String checksum,
@RequestParam(required = false) String checksum,
@RequestParam(required = false) String checksumAlgorithm,
@RequestBody MultipartFile firmwareFile) throws ThingsboardException {
@RequestBody MultipartFile file) throws ThingsboardException {
checkParameter(FIRMWARE_ID, strFirmwareId);
checkParameter("checksum", checksum);
try {
FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId));
FirmwareInfo info = checkFirmwareInfoId(firmwareId, Operation.READ);
@ -130,9 +129,9 @@ public class FirmwareController extends BaseController {
firmware.setChecksumAlgorithm(checksumAlgorithm);
firmware.setChecksum(checksum);
firmware.setFileName(firmwareFile.getOriginalFilename());
firmware.setContentType(firmwareFile.getContentType());
firmware.setData(ByteBuffer.wrap(firmwareFile.getBytes()));
firmware.setFileName(file.getOriginalFilename());
firmware.setContentType(file.getContentType());
firmware.setData(ByteBuffer.wrap(file.getBytes()));
return firmwareService.saveFirmware(firmware);
} catch (Exception e) {
throw handleException(e);
@ -186,4 +185,4 @@ public class FirmwareController extends BaseController {
}
}
}
}

9
dao/src/main/java/org/thingsboard/server/dao/firmware/BaseFirmwareService.java

@ -206,16 +206,16 @@ public class BaseFirmwareService implements FirmwareService {
throw new DataValidationException("Firmware content type should be specified!");
}
if (StringUtils.isEmpty(firmware.getChecksum())) {
throw new DataValidationException("Firmware checksum should be specified!");
}
ByteBuffer data = firmware.getData();
if (data == null || !data.hasArray() || data.array().length == 0) {
throw new DataValidationException("Firmware data should be specified!");
}
if (firmware.getChecksumAlgorithm() != null) {
if (StringUtils.isEmpty(firmware.getChecksum())) {
throw new DataValidationException("Firmware checksum should be specified!");
}
HashFunction hashFunction;
switch (firmware.getChecksumAlgorithm()) {
case "sha256":
@ -232,7 +232,6 @@ public class BaseFirmwareService implements FirmwareService {
}
String currentChecksum = hashFunction.hashBytes(data.array()).toString();
;
if (!currentChecksum.equals(firmware.getChecksum())) {
throw new DataValidationException("Wrong firmware file!");

123
ui-ngx/src/app/core/http/firmware.service.ts

@ -0,0 +1,123 @@
///
/// 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Firmware, FirmwareInfo } from '@shared/models/firmware.models';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
@Injectable({
providedIn: 'root'
})
export class FirmwareService {
constructor(
private http: HttpClient
) {
}
public getFirmwares(pageLink: PageLink, hasData?: boolean, config?: RequestConfig): Observable<PageData<FirmwareInfo>> {
let url = `/api/firmwares${pageLink.toQuery()}`;
if (isDefinedAndNotNull(hasData)) {
url += `&hasData=${hasData}`;
}
return this.http.get<PageData<FirmwareInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getFirmware(firmwareId: string, config?: RequestConfig): Observable<Firmware> {
return this.http.get<Firmware>(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config));
}
public getFirmwareInfo(firmwareId: string, config?: RequestConfig): Observable<FirmwareInfo> {
return this.http.get<FirmwareInfo>(`/api/firmware/info/${firmwareId}`, defaultHttpOptionsFromConfig(config));
}
public downloadFirmware(firmwareId: string): Observable<any> {
return this.http.get(`/api/firmware/${firmwareId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe(
map((response) => {
const headers = response.headers;
const filename = headers.get('x-filename');
const contentType = headers.get('content-type');
const linkElement = document.createElement('a');
try {
const blob = new Blob([response.body], { type: contentType });
const url = URL.createObjectURL(blob);
linkElement.setAttribute('href', url);
linkElement.setAttribute('download', filename);
const clickEvent = new MouseEvent('click',
{
view: window,
bubbles: true,
cancelable: false
}
);
linkElement.dispatchEvent(clickEvent);
return null;
} catch (e) {
throw e;
}
})
);
}
public saveFirmware(firmware: Firmware, config?: RequestConfig): Observable<Firmware> {
if (!firmware.file) {
return this.saveFirmwareInfo(firmware, config);
}
const firmwareInfo = deepClone(firmware);
delete firmwareInfo.file;
delete firmwareInfo.checksum;
delete firmwareInfo.checksumAlgorithm;
return this.saveFirmwareInfo(firmwareInfo, config).pipe(
mergeMap(res => {
return this.uploadFirmwareFile(res.id.id, firmware.file, firmware.checksumAlgorithm, firmware.checksum).pipe(
catchError(() => this.deleteFirmware(res.id.id))
);
})
);
}
public saveFirmwareInfo(firmware: FirmwareInfo, config?: RequestConfig): Observable<Firmware> {
return this.http.post<Firmware>('/api/firmware', firmware, defaultHttpOptionsFromConfig(config));
}
public uploadFirmwareFile(firmwareId: string, file: File, checksumAlgorithm?: string,
checksum?: string, config?: RequestConfig): Observable<any> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', file);
let url = `/api/firmware/${firmwareId}`;
if (checksumAlgorithm && checksum) {
url += `?checksumAlgorithm=${checksumAlgorithm}&checksum=${checksum}`;
}
return this.http.post(url, formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public deleteFirmware(firmwareId: string, config?: RequestConfig) {
return this.http.delete(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config));
}
}

8
ui-ngx/src/app/core/http/http-utils.ts

@ -39,3 +39,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false,
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest))
};
}
export function defaultHttpUploadOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false,
resendRequest: boolean = false) {
return {
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest))
};
}

12
ui-ngx/src/app/core/services/menu.service.ts

@ -279,6 +279,13 @@ export class MenuService {
icon: 'mdi:alpha-d-box',
isMdiIcon: true
},
{
id: guid(),
name: 'firmware.firmware',
type: 'link',
path: '/firmwares',
icon: 'memory'
},
{
id: guid(),
name: 'entity-view.entity-views',
@ -379,6 +386,11 @@ export class MenuService {
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/deviceProfiles'
},
{
name: 'firmware.firmware',
icon: 'memory',
path: '/firmwares'
}
]
},

2
ui-ngx/src/app/core/utils.ts

@ -407,7 +407,7 @@ export function sortObjectKeys<T>(obj: T): T {
}
export function deepTrim<T>(obj: T): T {
if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null) {
if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null || obj instanceof File) {
return obj;
}
return Object.keys(obj).reduce((acc, curr) => {

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

@ -172,7 +172,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
deleteEntityContent: EntityStringFunction<L> = () => '';
deleteEntitiesTitle: EntityCountStringFunction = () => '';
deleteEntitiesContent: EntityCountStringFunction = () => '';
loadEntity: EntityByIdOperation<T> = () => of();
loadEntity: EntityByIdOperation<T | L> = () => of();
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
deleteEntity: EntityIdOneWayOperation = () => of();
entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());

48
ui-ngx/src/app/modules/home/pages/firmware/firmware-routing.module.ts

@ -0,0 +1,48 @@
///
/// 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 { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core';
import { FirmwareTableConfigResolve } from '@home/pages/firmware/firmware-table-config.resolve';
const routes: Routes = [
{
path: 'firmwares',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'firmware.firmware',
breadcrumb: {
label: 'firmware.firmware',
icon: 'memory'
}
},
resolve: {
entitiesTableConfig: FirmwareTableConfigResolve
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
FirmwareTableConfigResolve
]
})
export class FirmwareRoutingModule{ }

99
ui-ngx/src/app/modules/home/pages/firmware/firmware-table-config.resolve.ts

@ -0,0 +1,99 @@
///
/// 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 { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import {
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { Firmware, FirmwareInfo } from '@shared/models/firmware.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { FirmwareService } from '@core/http/firmware.service';
import { PageLink } from '@shared/models/page/page-link';
import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { DeviceInfo } from '@shared/models/device.models';
@Injectable()
export class FirmwareTableConfigResolve implements Resolve<EntityTableConfig<Firmware, PageLink, FirmwareInfo>> {
private readonly config: EntityTableConfig<Firmware, PageLink, FirmwareInfo> = new EntityTableConfig<Firmware, PageLink, FirmwareInfo>();
constructor(private translate: TranslateService,
private datePipe: DatePipe,
private firmwareService: FirmwareService) {
this.config.entityType = EntityType.FIRMWARE;
this.config.entityComponent = FirmwaresComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.FIRMWARE);
this.config.entityResources = entityTypeResources.get(EntityType.FIRMWARE);
this.config.entityTitle = (firmware) => firmware ? firmware.title : '';
this.config.columns.push(
new DateEntityTableColumn<FirmwareInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<FirmwareInfo>('title', 'firmware.title', '50%'),
new EntityTableColumn<FirmwareInfo>('version', 'firmware.version', '50%')
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('firmware.export'),
icon: 'file_download',
isEnabled: (firmware) => firmware.hasData,
onAction: ($event, entity) => this.exportFirmware($event, entity)
}
);
this.config.deleteEntityTitle = firmware => this.translate.instant('firmware.delete-firmware-title',
{ firmwareTitle: firmware.title });
this.config.deleteEntityContent = () => this.translate.instant('firmware.delete-firmware-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('firmware.delete-firmwares-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('firmware.delete-firmwares-text');
this.config.entitiesFetchFunction = pageLink => this.firmwareService.getFirmwares(pageLink);
this.config.loadEntity = id => this.firmwareService.getFirmwareInfo(id.id);
this.config.saveEntity = firmware => this.firmwareService.saveFirmware(firmware);
this.config.deleteEntity = id => this.firmwareService.deleteFirmware(id.id);
this.config.onEntityAction = action => this.onFirmwareAction(action);
}
resolve(): EntityTableConfig<Firmware, PageLink, FirmwareInfo> {
this.config.tableTitle = this.translate.instant('firmware.firmware');
return this.config;
}
exportFirmware($event: Event, firmware: FirmwareInfo) {
if ($event) {
$event.stopPropagation();
}
this.firmwareService.downloadFirmware(firmware.id.id).subscribe();
}
onFirmwareAction(action: EntityAction<FirmwareInfo>): boolean {
switch (action.action) {
case 'uploadFirmware':
this.exportFirmware(action.event, action.entity);
return true;
}
return false;
}
}

35
ui-ngx/src/app/modules/home/pages/firmware/firmware.module.ts

@ -0,0 +1,35 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@home/components/home-components.module';
import { FirmwareRoutingModule } from '@home/pages/firmware/firmware-routing.module';
import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component';
@NgModule({
declarations: [
FirmwaresComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
FirmwareRoutingModule
]
})
export class FirmwareModule { }

87
ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html

@ -0,0 +1,87 @@
<!--
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.
-->
<div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async) || !entity?.hasData"
(click)="onEntityAction($event, 'uploadFirmware')"
[fxShow]="!isEdit">
{{'firmware.export' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'resource.delete' | translate }}
</button>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-hint class="tb-hint" translate *ngIf="isAdd">firmware.warning-after-save-no-edit</mat-hint>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex="45">
<mat-label translate>firmware.title</mat-label>
<input matInput formControlName="title" type="text" required>
<mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'firmware.title-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>firmware.version</mat-label>
<input matInput formControlName="version" type="text" required>
<mat-error *ngIf="entityForm.get('version').hasError('required')">
{{ 'firmware.version-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<section *ngIf="isAdd" style="padding-bottom: 8px">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex="45">
<mat-label translate>firmware.checksum-algorithm</mat-label>
<mat-select formControlName="checksumAlgorithm">
<mat-option [value]=null></mat-option>
<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>firmware.checksum</mat-label>
<input matInput formControlName="checksum" type="text"
[required]="entityForm.get('checksumAlgorithm').value != null">
<mat-error *ngIf="entityForm.get('checksumAlgorithm').hasError('required')">
{{ 'firmware.checksum-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<tb-file-input
formControlName="file"
workFromFileObj="true"
required
dropLabel="{{'resource.drop-file' | translate}}">
</tb-file-input>
</section>
<div formGroupName="additionalInfo">
<mat-form-field class="mat-block">
<mat-label translate>firmware.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</fieldset>
</form>
</div>

112
ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts

@ -0,0 +1,112 @@
///
/// 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 { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityComponent } from '@home/components/entity/entity.component';
import { ChecksumAlgorithm, ChecksumAlgorithmTranslationMap, Firmware } from '@shared/models/firmware.models';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-firmware',
templateUrl: './firmwares.component.html'
})
export class FirmwaresComponent extends EntityComponent<Firmware> implements OnInit, OnDestroy {
private destroy$ = new Subject();
checksumAlgorithms = Object.values(ChecksumAlgorithm);
checksumAlgorithmTranslationMap = ChecksumAlgorithmTranslationMap;
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: Firmware,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Firmware>,
public fb: FormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue);
}
ngOnInit() {
super.ngOnInit();
if (this.isAdd) {
this.entityForm.get('checksumAlgorithm').valueChanges.pipe(
map(algorithm => !!algorithm),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(
setAlgorithm => {
if (setAlgorithm) {
this.entityForm.get('checksum').setValidators([Validators.maxLength(1020), Validators.required]);
} else {
this.entityForm.get('checksum').clearValidators();
}
this.entityForm.get('checksum').updateValueAndValidity({emitEvent: false});
}
);
}
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: Firmware): FormGroup {
const form = this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
version: [entity ? entity.version : '', [Validators.required, Validators.maxLength(255)]],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
}
)
});
if (this.isAdd) {
form.addControl('checksumAlgorithm', this.fb.control(null));
form.addControl('checksum', this.fb.control('', Validators.maxLength(1020)));
form.addControl('file', this.fb.control(null, Validators.required));
}
return form;
}
updateForm(entity: Firmware) {
if (this.isEdit) {
this.entityForm.get('title').disable({emitEvent: false});
this.entityForm.get('version').disable({emitEvent: false});
}
this.entityForm.patchValue({
title: entity.title,
version: entity.version,
additionalInfo: {
description: entity.additionalInfo ? entity.additionalInfo.description : ''
}
});
}
}

2
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -35,6 +35,7 @@ import { modulesMap } from '../../common/modules-map';
import { DeviceProfileModule } from './device-profile/device-profile.module';
import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
import { ResourceModule } from '@home/pages/resource/resource.module';
import { FirmwareModule } from '@home/pages/firmware/firmware.module';
@NgModule({
exports: [
@ -54,6 +55,7 @@ import { ResourceModule } from '@home/pages/resource/resource.module';
AuditLogModule,
ApiUsageModule,
ResourceModule,
FirmwareModule,
UserModule
],
providers: [

55
ui-ngx/src/app/shared/components/file-input.component.ts

@ -105,6 +105,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input()
readAsBinary = false;
@Input()
workFromFileObj = false;
private multipleFileValue = false;
@Input()
@ -124,6 +127,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
fileName: string | string[];
fileContent: any;
files: File[];
@ViewChild('flow', {static: true})
flow: FlowDirective;
@ -151,15 +155,17 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
}
});
if (readers.length) {
Promise.all(readers).then((filesContent) => {
filesContent = filesContent.filter(content => content.fileContent != null);
if (filesContent.length === 1) {
this.fileContent = filesContent[0].fileContent;
this.fileName = filesContent[0].fileName;
Promise.all(readers).then((files) => {
files = files.filter(file => file.fileContent != null || file.files != null);
if (files.length === 1) {
this.fileContent = files[0].fileContent;
this.fileName = files[0].fileName;
this.files = files[0].files;
this.updateModel();
} else if (filesContent.length > 1) {
this.fileContent = filesContent.map(content => content.fileContent);
this.fileName = filesContent.map(content => content.fileName);
} else if (files.length > 1) {
this.fileContent = files.map(content => content.fileContent);
this.fileName = files.map(content => content.fileName);
this.files = files.map(content => content.files);
this.updateModel();
}
});
@ -177,21 +183,27 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
reader.onload = () => {
let fileName = null;
let fileContent = null;
let files = null;
if (typeof reader.result === 'string') {
fileContent = reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
if (fileContent) {
if (!this.workFromFileObj) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
if (fileContent) {
fileName = file.name;
}
} else {
files = file.file;
fileName = file.name;
}
}
}
resolve({fileContent, fileName});
resolve({fileContent, fileName, files});
};
reader.onerror = () => {
resolve({fileContent: null, fileName: null});
resolve({fileContent: null, fileName: null, files: null});
};
if (this.readAsBinary) {
reader.readAsBinaryString(file.file);
@ -227,7 +239,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
}
writeValue(value: any): void {
this.fileName = this.existingFileName || null;
let fileName = null;
if (this.workFromFileObj && value instanceof File) {
fileName = Array.isArray(value) ? value.map(file => file.name) : value.name;
}
this.fileName = this.existingFileName || fileName;
}
ngOnChanges(changes: SimpleChanges): void {
@ -242,13 +258,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
}
private updateModel() {
this.propagateChange(this.fileContent);
this.fileNameChanged.emit(this.fileName);
if (this.workFromFileObj) {
this.propagateChange(this.files);
} else {
this.propagateChange(this.fileContent);
this.fileNameChanged.emit(this.fileName);
}
}
clearFile() {
this.fileName = null;
this.fileContent = null;
this.files = null;
this.updateModel();
}

19
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -33,7 +33,8 @@ export enum EntityType {
WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
WIDGET_TYPE = 'WIDGET_TYPE',
API_USAGE_STATE = 'API_USAGE_STATE',
TB_RESOURCE = 'TB_RESOURCE'
TB_RESOURCE = 'TB_RESOURCE',
FIRMWARE = 'FIRMWARE'
}
export enum AliasEntityType {
@ -278,6 +279,16 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
selectedEntities: 'resource.selected-resources'
}
],
[
EntityType.FIRMWARE,
{
details: 'firmware.firmware-details',
add: 'firmware.add',
noEntities: 'firmware.no-firmware-text',
search: 'firmware.search',
selectedEntities: 'firmware.selected-firmware'
}
]
]
);
@ -354,6 +365,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
{
helpLinkId: 'resources'
}
],
[
EntityType.FIRMWARE,
{
helpLinkId: 'firmware'
}
]
]
);

50
ui-ngx/src/app/shared/models/firmware.models.ts

@ -0,0 +1,50 @@
///
/// 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 { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { FirmwareId } from '@shared/models/id/firmware-id';
export enum ChecksumAlgorithm {
MD5 = 'md5',
SHA256 = 'sha256',
CRC32 = 'crc32'
}
export const ChecksumAlgorithmTranslationMap = new Map<ChecksumAlgorithm, string>(
[
[ChecksumAlgorithm.MD5, 'MD5'],
[ChecksumAlgorithm.SHA256, 'SHA-256'],
[ChecksumAlgorithm.CRC32, 'CRC-32']
]
);
export interface FirmwareInfo extends BaseData<FirmwareId> {
tenantId?: TenantId;
title?: string;
version?: string;
hasData?: boolean;
additionalInfo?: any;
}
export interface Firmware extends FirmwareInfo {
file?: File;
data: string;
fileName: string;
checksum?: ChecksumAlgorithm;
checksumAlgorithm?: string;
contentType: string;
}

26
ui-ngx/src/app/shared/models/id/firmware-id.ts

@ -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.
///
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class FirmwareId implements EntityId {
entityType = EntityType.FIRMWARE;
id: string;
constructor(id: string) {
this.id = id;
}
}

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

@ -1693,6 +1693,33 @@
"inherit-owner": "Inherit from owner",
"source-attribute-not-set": "If source attribute isn't set"
},
"firmware": {
"add": "Add firmware",
"checksum": "Checksum",
"checksum-required": "Checksum is required.",
"checksum-algorithm": "Checksum algorithm",
"description": "Description",
"delete": "Delete firmware",
"delete-firmware-text": "Be careful, after the confirmation the firmware will become unrecoverable.",
"delete-firmware-title": "Are you sure you want to delete the firmware '{{firmwareTitle}}'?",
"delete-firmwares-action-title": "Delete { count, plural, 1 {1 firmware} other {# firmwares} }",
"delete-firmwares-text": "Be careful, after the confirmation all selected resources will be removed.",
"delete-firmwares-title": "Are you sure you want to delete { count, plural, 1 {1 firmware} other {# firmwares} }?",
"drop-file": "Drop a firmware file or click to select a file to upload.",
"empty": "Firmware is empty",
"export": "Export firmware",
"no-firmware-matching": "No firmware matching '{{firmware}}' were found.",
"no-firmware-text": "No firmwares found",
"firmware": "Firmware",
"firmware-details": "Firmware details",
"search": "Search firmwares",
"selected-firmware": "{ count, plural, 1 {1 firmware} other {# firmwares} } selected",
"title": "Title",
"title-required": "Title is required.",
"version": "Version",
"version-required": "Version is required.",
"warning-after-save-no-edit": "Once the firmware is saved, it will not be possible to change the title and version fields."
},
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",

Loading…
Cancel
Save