Showing
18 changed files
with
697 additions
and
32 deletions
... | ... | @@ -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!"); | ... | ... |
ui-ngx/src/app/core/http/firmware.service.ts
0 → 100644
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", | ... | ... |