Commit 45fc32ce3e946d1d5d586957c126ab368334256b

Authored by Vladyslav_Prykhodko
1 parent 3c4ee128

UI: Added firmware configuration

... ... @@ -112,11 +112,10 @@ public class FirmwareController extends BaseController {
112 112 @RequestMapping(value = "/firmware/{firmwareId}", method = RequestMethod.POST)
113 113 @ResponseBody
114 114 public Firmware saveFirmwareData(@PathVariable(FIRMWARE_ID) String strFirmwareId,
115   - @RequestParam String checksum,
  115 + @RequestParam(required = false) String checksum,
116 116 @RequestParam(required = false) String checksumAlgorithm,
117   - @RequestBody MultipartFile firmwareFile) throws ThingsboardException {
  117 + @RequestBody MultipartFile file) throws ThingsboardException {
118 118 checkParameter(FIRMWARE_ID, strFirmwareId);
119   - checkParameter("checksum", checksum);
120 119 try {
121 120 FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId));
122 121 FirmwareInfo info = checkFirmwareInfoId(firmwareId, Operation.READ);
... ... @@ -130,9 +129,9 @@ public class FirmwareController extends BaseController {
130 129
131 130 firmware.setChecksumAlgorithm(checksumAlgorithm);
132 131 firmware.setChecksum(checksum);
133   - firmware.setFileName(firmwareFile.getOriginalFilename());
134   - firmware.setContentType(firmwareFile.getContentType());
135   - firmware.setData(ByteBuffer.wrap(firmwareFile.getBytes()));
  132 + firmware.setFileName(file.getOriginalFilename());
  133 + firmware.setContentType(file.getContentType());
  134 + firmware.setData(ByteBuffer.wrap(file.getBytes()));
136 135 return firmwareService.saveFirmware(firmware);
137 136 } catch (Exception e) {
138 137 throw handleException(e);
... ... @@ -186,4 +185,4 @@ public class FirmwareController extends BaseController {
186 185 }
187 186 }
188 187
189   -}
\ No newline at end of file
  188 +}
... ...
... ... @@ -206,16 +206,16 @@ public class BaseFirmwareService implements FirmwareService {
206 206 throw new DataValidationException("Firmware content type should be specified!");
207 207 }
208 208
209   - if (StringUtils.isEmpty(firmware.getChecksum())) {
210   - throw new DataValidationException("Firmware checksum should be specified!");
211   - }
212   -
213 209 ByteBuffer data = firmware.getData();
214 210 if (data == null || !data.hasArray() || data.array().length == 0) {
215 211 throw new DataValidationException("Firmware data should be specified!");
216 212 }
217 213
218 214 if (firmware.getChecksumAlgorithm() != null) {
  215 + if (StringUtils.isEmpty(firmware.getChecksum())) {
  216 + throw new DataValidationException("Firmware checksum should be specified!");
  217 + }
  218 +
219 219 HashFunction hashFunction;
220 220 switch (firmware.getChecksumAlgorithm()) {
221 221 case "sha256":
... ... @@ -232,7 +232,6 @@ public class BaseFirmwareService implements FirmwareService {
232 232 }
233 233
234 234 String currentChecksum = hashFunction.hashBytes(data.array()).toString();
235   - ;
236 235
237 236 if (!currentChecksum.equals(firmware.getChecksum())) {
238 237 throw new DataValidationException("Wrong firmware file!");
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { HttpClient } from '@angular/common/http';
  19 +import { PageLink } from '@shared/models/page/page-link';
  20 +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
  21 +import { Observable } from 'rxjs';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Firmware, FirmwareInfo } from '@shared/models/firmware.models';
  24 +import { catchError, map, mergeMap } from 'rxjs/operators';
  25 +import { deepClone, isDefinedAndNotNull } from '@core/utils';
  26 +import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
  27 +import { InterceptorConfig } from '@core/interceptors/interceptor-config';
  28 +
  29 +@Injectable({
  30 + providedIn: 'root'
  31 +})
  32 +export class FirmwareService {
  33 + constructor(
  34 + private http: HttpClient
  35 + ) {
  36 +
  37 + }
  38 +
  39 + public getFirmwares(pageLink: PageLink, hasData?: boolean, config?: RequestConfig): Observable<PageData<FirmwareInfo>> {
  40 + let url = `/api/firmwares${pageLink.toQuery()}`;
  41 + if (isDefinedAndNotNull(hasData)) {
  42 + url += `&hasData=${hasData}`;
  43 + }
  44 + return this.http.get<PageData<FirmwareInfo>>(url, defaultHttpOptionsFromConfig(config));
  45 + }
  46 +
  47 + public getFirmware(firmwareId: string, config?: RequestConfig): Observable<Firmware> {
  48 + return this.http.get<Firmware>(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config));
  49 + }
  50 +
  51 + public getFirmwareInfo(firmwareId: string, config?: RequestConfig): Observable<FirmwareInfo> {
  52 + return this.http.get<FirmwareInfo>(`/api/firmware/info/${firmwareId}`, defaultHttpOptionsFromConfig(config));
  53 + }
  54 +
  55 + public downloadFirmware(firmwareId: string): Observable<any> {
  56 + return this.http.get(`/api/firmware/${firmwareId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe(
  57 + map((response) => {
  58 + const headers = response.headers;
  59 + const filename = headers.get('x-filename');
  60 + const contentType = headers.get('content-type');
  61 + const linkElement = document.createElement('a');
  62 + try {
  63 + const blob = new Blob([response.body], { type: contentType });
  64 + const url = URL.createObjectURL(blob);
  65 + linkElement.setAttribute('href', url);
  66 + linkElement.setAttribute('download', filename);
  67 + const clickEvent = new MouseEvent('click',
  68 + {
  69 + view: window,
  70 + bubbles: true,
  71 + cancelable: false
  72 + }
  73 + );
  74 + linkElement.dispatchEvent(clickEvent);
  75 + return null;
  76 + } catch (e) {
  77 + throw e;
  78 + }
  79 + })
  80 + );
  81 + }
  82 +
  83 + public saveFirmware(firmware: Firmware, config?: RequestConfig): Observable<Firmware> {
  84 + if (!firmware.file) {
  85 + return this.saveFirmwareInfo(firmware, config);
  86 + }
  87 + const firmwareInfo = deepClone(firmware);
  88 + delete firmwareInfo.file;
  89 + delete firmwareInfo.checksum;
  90 + delete firmwareInfo.checksumAlgorithm;
  91 + return this.saveFirmwareInfo(firmwareInfo, config).pipe(
  92 + mergeMap(res => {
  93 + return this.uploadFirmwareFile(res.id.id, firmware.file, firmware.checksumAlgorithm, firmware.checksum).pipe(
  94 + catchError(() => this.deleteFirmware(res.id.id))
  95 + );
  96 + })
  97 + );
  98 + }
  99 +
  100 + public saveFirmwareInfo(firmware: FirmwareInfo, config?: RequestConfig): Observable<Firmware> {
  101 + return this.http.post<Firmware>('/api/firmware', firmware, defaultHttpOptionsFromConfig(config));
  102 + }
  103 +
  104 + public uploadFirmwareFile(firmwareId: string, file: File, checksumAlgorithm?: string,
  105 + checksum?: string, config?: RequestConfig): Observable<any> {
  106 + if (!config) {
  107 + config = {};
  108 + }
  109 + const formData = new FormData();
  110 + formData.append('file', file);
  111 + let url = `/api/firmware/${firmwareId}`;
  112 + if (checksumAlgorithm && checksum) {
  113 + url += `?checksumAlgorithm=${checksumAlgorithm}&checksum=${checksum}`;
  114 + }
  115 + return this.http.post(url, formData,
  116 + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
  117 + }
  118 +
  119 + public deleteFirmware(firmwareId: string, config?: RequestConfig) {
  120 + return this.http.delete(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config));
  121 + }
  122 +
  123 +}
... ...
... ... @@ -39,3 +39,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false,
39 39 params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest))
40 40 };
41 41 }
  42 +
  43 +export function defaultHttpUploadOptions(ignoreLoading: boolean = false,
  44 + ignoreErrors: boolean = false,
  45 + resendRequest: boolean = false) {
  46 + return {
  47 + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest))
  48 + };
  49 +}
... ...
... ... @@ -281,6 +281,13 @@ export class MenuService {
281 281 },
282 282 {
283 283 id: guid(),
  284 + name: 'firmware.firmware',
  285 + type: 'link',
  286 + path: '/firmwares',
  287 + icon: 'memory'
  288 + },
  289 + {
  290 + id: guid(),
284 291 name: 'entity-view.entity-views',
285 292 type: 'link',
286 293 path: '/entityViews',
... ... @@ -379,6 +386,11 @@ export class MenuService {
379 386 icon: 'mdi:alpha-d-box',
380 387 isMdiIcon: true,
381 388 path: '/deviceProfiles'
  389 + },
  390 + {
  391 + name: 'firmware.firmware',
  392 + icon: 'memory',
  393 + path: '/firmwares'
382 394 }
383 395 ]
384 396 },
... ...
... ... @@ -407,7 +407,7 @@ export function sortObjectKeys<T>(obj: T): T {
407 407 }
408 408
409 409 export function deepTrim<T>(obj: T): T {
410   - if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null) {
  410 + if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null || obj instanceof File) {
411 411 return obj;
412 412 }
413 413 return Object.keys(obj).reduce((acc, curr) => {
... ...
... ... @@ -172,7 +172,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
172 172 deleteEntityContent: EntityStringFunction<L> = () => '';
173 173 deleteEntitiesTitle: EntityCountStringFunction = () => '';
174 174 deleteEntitiesContent: EntityCountStringFunction = () => '';
175   - loadEntity: EntityByIdOperation<T> = () => of();
  175 + loadEntity: EntityByIdOperation<T | L> = () => of();
176 176 saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
177 177 deleteEntity: EntityIdOneWayOperation = () => of();
178 178 entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { RouterModule, Routes } from '@angular/router';
  18 +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
  19 +import { Authority } from '@shared/models/authority.enum';
  20 +import { NgModule } from '@angular/core';
  21 +import { FirmwareTableConfigResolve } from '@home/pages/firmware/firmware-table-config.resolve';
  22 +
  23 +const routes: Routes = [
  24 + {
  25 + path: 'firmwares',
  26 + component: EntitiesTableComponent,
  27 + data: {
  28 + auth: [Authority.TENANT_ADMIN],
  29 + title: 'firmware.firmware',
  30 + breadcrumb: {
  31 + label: 'firmware.firmware',
  32 + icon: 'memory'
  33 + }
  34 + },
  35 + resolve: {
  36 + entitiesTableConfig: FirmwareTableConfigResolve
  37 + }
  38 + }
  39 +];
  40 +
  41 +@NgModule({
  42 + imports: [RouterModule.forChild(routes)],
  43 + exports: [RouterModule],
  44 + providers: [
  45 + FirmwareTableConfigResolve
  46 + ]
  47 +})
  48 +export class FirmwareRoutingModule{ }
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { Resolve } from '@angular/router';
  19 +import {
  20 + DateEntityTableColumn,
  21 + EntityTableColumn,
  22 + EntityTableConfig
  23 +} from '@home/models/entity/entities-table-config.models';
  24 +import { Firmware, FirmwareInfo } from '@shared/models/firmware.models';
  25 +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { DatePipe } from '@angular/common';
  28 +import { FirmwareService } from '@core/http/firmware.service';
  29 +import { PageLink } from '@shared/models/page/page-link';
  30 +import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component';
  31 +import { EntityAction } from '@home/models/entity/entity-component.models';
  32 +import { DeviceInfo } from '@shared/models/device.models';
  33 +
  34 +@Injectable()
  35 +export class FirmwareTableConfigResolve implements Resolve<EntityTableConfig<Firmware, PageLink, FirmwareInfo>> {
  36 +
  37 + private readonly config: EntityTableConfig<Firmware, PageLink, FirmwareInfo> = new EntityTableConfig<Firmware, PageLink, FirmwareInfo>();
  38 +
  39 + constructor(private translate: TranslateService,
  40 + private datePipe: DatePipe,
  41 + private firmwareService: FirmwareService) {
  42 + this.config.entityType = EntityType.FIRMWARE;
  43 + this.config.entityComponent = FirmwaresComponent;
  44 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.FIRMWARE);
  45 + this.config.entityResources = entityTypeResources.get(EntityType.FIRMWARE);
  46 +
  47 + this.config.entityTitle = (firmware) => firmware ? firmware.title : '';
  48 +
  49 + this.config.columns.push(
  50 + new DateEntityTableColumn<FirmwareInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
  51 + new EntityTableColumn<FirmwareInfo>('title', 'firmware.title', '50%'),
  52 + new EntityTableColumn<FirmwareInfo>('version', 'firmware.version', '50%')
  53 + );
  54 +
  55 + this.config.cellActionDescriptors.push(
  56 + {
  57 + name: this.translate.instant('firmware.export'),
  58 + icon: 'file_download',
  59 + isEnabled: (firmware) => firmware.hasData,
  60 + onAction: ($event, entity) => this.exportFirmware($event, entity)
  61 + }
  62 + );
  63 +
  64 + this.config.deleteEntityTitle = firmware => this.translate.instant('firmware.delete-firmware-title',
  65 + { firmwareTitle: firmware.title });
  66 + this.config.deleteEntityContent = () => this.translate.instant('firmware.delete-firmware-text');
  67 + this.config.deleteEntitiesTitle = count => this.translate.instant('firmware.delete-firmwares-title', {count});
  68 + this.config.deleteEntitiesContent = () => this.translate.instant('firmware.delete-firmwares-text');
  69 +
  70 + this.config.entitiesFetchFunction = pageLink => this.firmwareService.getFirmwares(pageLink);
  71 + this.config.loadEntity = id => this.firmwareService.getFirmwareInfo(id.id);
  72 + this.config.saveEntity = firmware => this.firmwareService.saveFirmware(firmware);
  73 + this.config.deleteEntity = id => this.firmwareService.deleteFirmware(id.id);
  74 +
  75 + this.config.onEntityAction = action => this.onFirmwareAction(action);
  76 + }
  77 +
  78 + resolve(): EntityTableConfig<Firmware, PageLink, FirmwareInfo> {
  79 + this.config.tableTitle = this.translate.instant('firmware.firmware');
  80 + return this.config;
  81 + }
  82 +
  83 + exportFirmware($event: Event, firmware: FirmwareInfo) {
  84 + if ($event) {
  85 + $event.stopPropagation();
  86 + }
  87 + this.firmwareService.downloadFirmware(firmware.id.id).subscribe();
  88 + }
  89 +
  90 + onFirmwareAction(action: EntityAction<FirmwareInfo>): boolean {
  91 + switch (action.action) {
  92 + case 'uploadFirmware':
  93 + this.exportFirmware(action.event, action.entity);
  94 + return true;
  95 + }
  96 + return false;
  97 + }
  98 +
  99 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import { HomeComponentsModule } from '@home/components/home-components.module';
  21 +import { FirmwareRoutingModule } from '@home/pages/firmware/firmware-routing.module';
  22 +import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component';
  23 +
  24 +@NgModule({
  25 + declarations: [
  26 + FirmwaresComponent
  27 + ],
  28 + imports: [
  29 + CommonModule,
  30 + SharedModule,
  31 + HomeComponentsModule,
  32 + FirmwareRoutingModule
  33 + ]
  34 +})
  35 +export class FirmwareModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons" fxLayout.xs="column">
  19 + <button mat-raised-button color="primary" fxFlex.xs
  20 + [disabled]="(isLoading$ | async) || !entity?.hasData"
  21 + (click)="onEntityAction($event, 'uploadFirmware')"
  22 + [fxShow]="!isEdit">
  23 + {{'firmware.export' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary" fxFlex.xs
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'delete')"
  28 + [fxShow]="!hideDelete() && !isEdit">
  29 + {{'resource.delete' | translate }}
  30 + </button>
  31 +</div>
  32 +<div class="mat-padding" fxLayout="column">
  33 + <form [formGroup]="entityForm">
  34 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  35 + <mat-hint class="tb-hint" translate *ngIf="isAdd">firmware.warning-after-save-no-edit</mat-hint>
  36 + <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
  37 + <mat-form-field class="mat-block" fxFlex="45">
  38 + <mat-label translate>firmware.title</mat-label>
  39 + <input matInput formControlName="title" type="text" required>
  40 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  41 + {{ 'firmware.title-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <mat-form-field class="mat-block" fxFlex>
  45 + <mat-label translate>firmware.version</mat-label>
  46 + <input matInput formControlName="version" type="text" required>
  47 + <mat-error *ngIf="entityForm.get('version').hasError('required')">
  48 + {{ 'firmware.version-required' | translate }}
  49 + </mat-error>
  50 + </mat-form-field>
  51 + </div>
  52 + <section *ngIf="isAdd" style="padding-bottom: 8px">
  53 + <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
  54 + <mat-form-field class="mat-block" fxFlex="45">
  55 + <mat-label translate>firmware.checksum-algorithm</mat-label>
  56 + <mat-select formControlName="checksumAlgorithm">
  57 + <mat-option [value]=null></mat-option>
  58 + <mat-option *ngFor="let checksumAlgorithm of checksumAlgorithms" [value]="checksumAlgorithm">
  59 + {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }}
  60 + </mat-option>
  61 + </mat-select>
  62 + </mat-form-field>
  63 + <mat-form-field class="mat-block" fxFlex>
  64 + <mat-label translate>firmware.checksum</mat-label>
  65 + <input matInput formControlName="checksum" type="text"
  66 + [required]="entityForm.get('checksumAlgorithm').value != null">
  67 + <mat-error *ngIf="entityForm.get('checksumAlgorithm').hasError('required')">
  68 + {{ 'firmware.checksum-required' | translate }}
  69 + </mat-error>
  70 + </mat-form-field>
  71 + </div>
  72 + <tb-file-input
  73 + formControlName="file"
  74 + workFromFileObj="true"
  75 + required
  76 + dropLabel="{{'resource.drop-file' | translate}}">
  77 + </tb-file-input>
  78 + </section>
  79 + <div formGroupName="additionalInfo">
  80 + <mat-form-field class="mat-block">
  81 + <mat-label translate>firmware.description</mat-label>
  82 + <textarea matInput formControlName="description" rows="2"></textarea>
  83 + </mat-form-field>
  84 + </div>
  85 + </fieldset>
  86 + </form>
  87 +</div>
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
  18 +import { Subject } from 'rxjs';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  23 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  24 +import { EntityComponent } from '@home/components/entity/entity.component';
  25 +import { ChecksumAlgorithm, ChecksumAlgorithmTranslationMap, Firmware } from '@shared/models/firmware.models';
  26 +import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
  27 +
  28 +@Component({
  29 + selector: 'tb-firmware',
  30 + templateUrl: './firmwares.component.html'
  31 +})
  32 +export class FirmwaresComponent extends EntityComponent<Firmware> implements OnInit, OnDestroy {
  33 +
  34 + private destroy$ = new Subject();
  35 +
  36 + checksumAlgorithms = Object.values(ChecksumAlgorithm);
  37 + checksumAlgorithmTranslationMap = ChecksumAlgorithmTranslationMap;
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + protected translate: TranslateService,
  41 + @Inject('entity') protected entityValue: Firmware,
  42 + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Firmware>,
  43 + public fb: FormBuilder) {
  44 + super(store, fb, entityValue, entitiesTableConfigValue);
  45 + }
  46 +
  47 + ngOnInit() {
  48 + super.ngOnInit();
  49 + if (this.isAdd) {
  50 + this.entityForm.get('checksumAlgorithm').valueChanges.pipe(
  51 + map(algorithm => !!algorithm),
  52 + distinctUntilChanged(),
  53 + takeUntil(this.destroy$)
  54 + ).subscribe(
  55 + setAlgorithm => {
  56 + if (setAlgorithm) {
  57 + this.entityForm.get('checksum').setValidators([Validators.maxLength(1020), Validators.required]);
  58 + } else {
  59 + this.entityForm.get('checksum').clearValidators();
  60 + }
  61 + this.entityForm.get('checksum').updateValueAndValidity({emitEvent: false});
  62 + }
  63 + );
  64 + }
  65 + }
  66 +
  67 + ngOnDestroy() {
  68 + super.ngOnDestroy();
  69 + this.destroy$.next();
  70 + this.destroy$.complete();
  71 + }
  72 +
  73 + hideDelete() {
  74 + if (this.entitiesTableConfig) {
  75 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  76 + } else {
  77 + return false;
  78 + }
  79 + }
  80 +
  81 + buildForm(entity: Firmware): FormGroup {
  82 + const form = this.fb.group({
  83 + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
  84 + version: [entity ? entity.version : '', [Validators.required, Validators.maxLength(255)]],
  85 + additionalInfo: this.fb.group(
  86 + {
  87 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  88 + }
  89 + )
  90 + });
  91 + if (this.isAdd) {
  92 + form.addControl('checksumAlgorithm', this.fb.control(null));
  93 + form.addControl('checksum', this.fb.control('', Validators.maxLength(1020)));
  94 + form.addControl('file', this.fb.control(null, Validators.required));
  95 + }
  96 + return form;
  97 + }
  98 +
  99 + updateForm(entity: Firmware) {
  100 + if (this.isEdit) {
  101 + this.entityForm.get('title').disable({emitEvent: false});
  102 + this.entityForm.get('version').disable({emitEvent: false});
  103 + }
  104 + this.entityForm.patchValue({
  105 + title: entity.title,
  106 + version: entity.version,
  107 + additionalInfo: {
  108 + description: entity.additionalInfo ? entity.additionalInfo.description : ''
  109 + }
  110 + });
  111 + }
  112 +}
... ...
... ... @@ -35,6 +35,7 @@ import { modulesMap } from '../../common/modules-map';
35 35 import { DeviceProfileModule } from './device-profile/device-profile.module';
36 36 import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
37 37 import { ResourceModule } from '@home/pages/resource/resource.module';
  38 +import { FirmwareModule } from '@home/pages/firmware/firmware.module';
38 39
39 40 @NgModule({
40 41 exports: [
... ... @@ -54,6 +55,7 @@ import { ResourceModule } from '@home/pages/resource/resource.module';
54 55 AuditLogModule,
55 56 ApiUsageModule,
56 57 ResourceModule,
  58 + FirmwareModule,
57 59 UserModule
58 60 ],
59 61 providers: [
... ...
... ... @@ -105,6 +105,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
105 105 @Input()
106 106 readAsBinary = false;
107 107
  108 + @Input()
  109 + workFromFileObj = false;
  110 +
108 111 private multipleFileValue = false;
109 112
110 113 @Input()
... ... @@ -124,6 +127,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
124 127
125 128 fileName: string | string[];
126 129 fileContent: any;
  130 + files: File[];
127 131
128 132 @ViewChild('flow', {static: true})
129 133 flow: FlowDirective;
... ... @@ -151,15 +155,17 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
151 155 }
152 156 });
153 157 if (readers.length) {
154   - Promise.all(readers).then((filesContent) => {
155   - filesContent = filesContent.filter(content => content.fileContent != null);
156   - if (filesContent.length === 1) {
157   - this.fileContent = filesContent[0].fileContent;
158   - this.fileName = filesContent[0].fileName;
  158 + Promise.all(readers).then((files) => {
  159 + files = files.filter(file => file.fileContent != null || file.files != null);
  160 + if (files.length === 1) {
  161 + this.fileContent = files[0].fileContent;
  162 + this.fileName = files[0].fileName;
  163 + this.files = files[0].files;
159 164 this.updateModel();
160   - } else if (filesContent.length > 1) {
161   - this.fileContent = filesContent.map(content => content.fileContent);
162   - this.fileName = filesContent.map(content => content.fileName);
  165 + } else if (files.length > 1) {
  166 + this.fileContent = files.map(content => content.fileContent);
  167 + this.fileName = files.map(content => content.fileName);
  168 + this.files = files.map(content => content.files);
163 169 this.updateModel();
164 170 }
165 171 });
... ... @@ -177,21 +183,27 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
177 183 reader.onload = () => {
178 184 let fileName = null;
179 185 let fileContent = null;
  186 + let files = null;
180 187 if (typeof reader.result === 'string') {
181 188 fileContent = reader.result;
182 189 if (fileContent && fileContent.length > 0) {
183   - if (this.contentConvertFunction) {
184   - fileContent = this.contentConvertFunction(fileContent);
185   - }
186   - if (fileContent) {
  190 + if (!this.workFromFileObj) {
  191 + if (this.contentConvertFunction) {
  192 + fileContent = this.contentConvertFunction(fileContent);
  193 + }
  194 + if (fileContent) {
  195 + fileName = file.name;
  196 + }
  197 + } else {
  198 + files = file.file;
187 199 fileName = file.name;
188 200 }
189 201 }
190 202 }
191   - resolve({fileContent, fileName});
  203 + resolve({fileContent, fileName, files});
192 204 };
193 205 reader.onerror = () => {
194   - resolve({fileContent: null, fileName: null});
  206 + resolve({fileContent: null, fileName: null, files: null});
195 207 };
196 208 if (this.readAsBinary) {
197 209 reader.readAsBinaryString(file.file);
... ... @@ -227,7 +239,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
227 239 }
228 240
229 241 writeValue(value: any): void {
230   - this.fileName = this.existingFileName || null;
  242 + let fileName = null;
  243 + if (this.workFromFileObj && value instanceof File) {
  244 + fileName = Array.isArray(value) ? value.map(file => file.name) : value.name;
  245 + }
  246 + this.fileName = this.existingFileName || fileName;
231 247 }
232 248
233 249 ngOnChanges(changes: SimpleChanges): void {
... ... @@ -242,13 +258,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
242 258 }
243 259
244 260 private updateModel() {
245   - this.propagateChange(this.fileContent);
246   - this.fileNameChanged.emit(this.fileName);
  261 + if (this.workFromFileObj) {
  262 + this.propagateChange(this.files);
  263 + } else {
  264 + this.propagateChange(this.fileContent);
  265 + this.fileNameChanged.emit(this.fileName);
  266 + }
247 267 }
248 268
249 269 clearFile() {
250 270 this.fileName = null;
251 271 this.fileContent = null;
  272 + this.files = null;
252 273 this.updateModel();
253 274 }
254 275
... ...
... ... @@ -33,7 +33,8 @@ export enum EntityType {
33 33 WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
34 34 WIDGET_TYPE = 'WIDGET_TYPE',
35 35 API_USAGE_STATE = 'API_USAGE_STATE',
36   - TB_RESOURCE = 'TB_RESOURCE'
  36 + TB_RESOURCE = 'TB_RESOURCE',
  37 + FIRMWARE = 'FIRMWARE'
37 38 }
38 39
39 40 export enum AliasEntityType {
... ... @@ -278,6 +279,16 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
278 279 selectedEntities: 'resource.selected-resources'
279 280 }
280 281 ],
  282 + [
  283 + EntityType.FIRMWARE,
  284 + {
  285 + details: 'firmware.firmware-details',
  286 + add: 'firmware.add',
  287 + noEntities: 'firmware.no-firmware-text',
  288 + search: 'firmware.search',
  289 + selectedEntities: 'firmware.selected-firmware'
  290 + }
  291 + ]
281 292 ]
282 293 );
283 294
... ... @@ -354,6 +365,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
354 365 {
355 366 helpLinkId: 'resources'
356 367 }
  368 + ],
  369 + [
  370 + EntityType.FIRMWARE,
  371 + {
  372 + helpLinkId: 'firmware'
  373 + }
357 374 ]
358 375 ]
359 376 );
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { BaseData } from '@shared/models/base-data';
  18 +import { TenantId } from '@shared/models/id/tenant-id';
  19 +import { FirmwareId } from '@shared/models/id/firmware-id';
  20 +
  21 +export enum ChecksumAlgorithm {
  22 + MD5 = 'md5',
  23 + SHA256 = 'sha256',
  24 + CRC32 = 'crc32'
  25 +}
  26 +
  27 +export const ChecksumAlgorithmTranslationMap = new Map<ChecksumAlgorithm, string>(
  28 + [
  29 + [ChecksumAlgorithm.MD5, 'MD5'],
  30 + [ChecksumAlgorithm.SHA256, 'SHA-256'],
  31 + [ChecksumAlgorithm.CRC32, 'CRC-32']
  32 + ]
  33 +);
  34 +
  35 +export interface FirmwareInfo extends BaseData<FirmwareId> {
  36 + tenantId?: TenantId;
  37 + title?: string;
  38 + version?: string;
  39 + hasData?: boolean;
  40 + additionalInfo?: any;
  41 +}
  42 +
  43 +export interface Firmware extends FirmwareInfo {
  44 + file?: File;
  45 + data: string;
  46 + fileName: string;
  47 + checksum?: ChecksumAlgorithm;
  48 + checksumAlgorithm?: string;
  49 + contentType: string;
  50 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { EntityId } from '@shared/models/id/entity-id';
  18 +import { EntityType } from '@shared/models/entity-type.models';
  19 +
  20 +export class FirmwareId implements EntityId {
  21 + entityType = EntityType.FIRMWARE;
  22 + id: string;
  23 + constructor(id: string) {
  24 + this.id = id;
  25 + }
  26 +}
... ...
... ... @@ -1693,6 +1693,33 @@
1693 1693 "inherit-owner": "Inherit from owner",
1694 1694 "source-attribute-not-set": "If source attribute isn't set"
1695 1695 },
  1696 + "firmware": {
  1697 + "add": "Add firmware",
  1698 + "checksum": "Checksum",
  1699 + "checksum-required": "Checksum is required.",
  1700 + "checksum-algorithm": "Checksum algorithm",
  1701 + "description": "Description",
  1702 + "delete": "Delete firmware",
  1703 + "delete-firmware-text": "Be careful, after the confirmation the firmware will become unrecoverable.",
  1704 + "delete-firmware-title": "Are you sure you want to delete the firmware '{{firmwareTitle}}'?",
  1705 + "delete-firmwares-action-title": "Delete { count, plural, 1 {1 firmware} other {# firmwares} }",
  1706 + "delete-firmwares-text": "Be careful, after the confirmation all selected resources will be removed.",
  1707 + "delete-firmwares-title": "Are you sure you want to delete { count, plural, 1 {1 firmware} other {# firmwares} }?",
  1708 + "drop-file": "Drop a firmware file or click to select a file to upload.",
  1709 + "empty": "Firmware is empty",
  1710 + "export": "Export firmware",
  1711 + "no-firmware-matching": "No firmware matching '{{firmware}}' were found.",
  1712 + "no-firmware-text": "No firmwares found",
  1713 + "firmware": "Firmware",
  1714 + "firmware-details": "Firmware details",
  1715 + "search": "Search firmwares",
  1716 + "selected-firmware": "{ count, plural, 1 {1 firmware} other {# firmwares} } selected",
  1717 + "title": "Title",
  1718 + "title-required": "Title is required.",
  1719 + "version": "Version",
  1720 + "version-required": "Version is required.",
  1721 + "warning-after-save-no-edit": "Once the firmware is saved, it will not be possible to change the title and version fields."
  1722 + },
1696 1723 "fullscreen": {
1697 1724 "expand": "Expand to fullscreen",
1698 1725 "exit": "Exit fullscreen",
... ...