diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 0862e10a76..2d9b9808ed 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -74,6 +74,7 @@ import { StringOperation } from '@shared/models/query/query.models'; import { alarmFields } from '@shared/models/alarm.models'; +import { FirmwareService } from '@core/http/firmware.service'; @Injectable({ providedIn: 'root' @@ -93,6 +94,7 @@ export class EntityService { private dashboardService: DashboardService, private entityRelationService: EntityRelationService, private attributeService: AttributeService, + private firmwareService: FirmwareService, private utils: UtilsService ) { } @@ -128,6 +130,9 @@ export class EntityService { case EntityType.ALARM: console.error('Get Alarm Entity is not implemented!'); break; + case EntityType.FIRMWARE: + observable = this.firmwareService.getFirmwareInfo(entityId, config); + break; } return observable; } @@ -326,6 +331,10 @@ export class EntityService { case EntityType.ALARM: console.error('Get Alarm Entities is not implemented!'); break; + case EntityType.FIRMWARE: + pageLink.sortOrder.property = 'title'; + entitiesObservable = this.firmwareService.getFirmwares(pageLink, true, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/firmware.service.ts b/ui-ngx/src/app/core/http/firmware.service.ts index 0177cd4cfc..7bf42fb49e 100644 --- a/ui-ngx/src/app/core/http/firmware.service.ts +++ b/ui-ngx/src/app/core/http/firmware.service.ts @@ -23,8 +23,6 @@ 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' @@ -37,10 +35,11 @@ export class FirmwareService { } public getFirmwares(pageLink: PageLink, hasData?: boolean, config?: RequestConfig): Observable> { - let url = `/api/firmwares${pageLink.toQuery()}`; + let url = `/api/firmwares`; if (isDefinedAndNotNull(hasData)) { - url += `&hasData=${hasData}`; + url += `/${hasData}`; } + url += `${pageLink.toQuery()}`; return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html new file mode 100644 index 0000000000..4e733b3132 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html @@ -0,0 +1,54 @@ + + + + + + + + + +
+
+ firmware.no-firmware-text +
+ + + {{ translate.get('firmware.no-firmware-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + +
+
+
+ + {{ requiredErrorText | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts new file mode 100644 index 0000000000..79dc170c7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts @@ -0,0 +1,236 @@ +/// +/// 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, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { FirmwareInfo } from '@shared/models/firmware.models'; +import { FirmwareService } from '@core/http/firmware.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; + +@Component({ + selector: 'tb-firmware-autocomplete', + templateUrl: './firmware-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FirmwareAutocompleteComponent), + multi: true + }] +}) +export class FirmwareAutocompleteComponent implements ControlValueAccessor, OnInit { + + firmwareFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + @Input() + useFullEntityId = false; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('firmwareInput', {static: true}) firmwareInput: ElementRef; + @ViewChild('firmwareInput', {read: MatAutocompleteTrigger}) firmwareAutocomplete: MatAutocompleteTrigger; + + filteredFirmwares: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private firmwareService: FirmwareService, + private fb: FormBuilder) { + this.firmwareFormGroup = this.fb.group({ + firmwareId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredFirmwares = this.firmwareFormGroup.get('firmwareId').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = this.useFullEntityId ? value.id : value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.title) : ''), + mergeMap(name => this.fetchFirmware(name)), + share() + ); + } + + ngAfterViewInit(): void { + } + + getCurrentEntity(): BaseData | null { + const currentRuleChain = this.firmwareFormGroup.get('firmwareId').value; + if (currentRuleChain && typeof currentRuleChain !== 'string') { + return currentRuleChain as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.firmwareFormGroup.disable({emitEvent: false}); + } else { + this.firmwareFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | EntityId | null): void { + this.searchText = ''; + if (value != null && value !== '') { + let firmwareId = ''; + if (typeof value === 'string') { + firmwareId = value; + } else if (value.entityType && value.id) { + firmwareId = value.id; + } + if (firmwareId !== '') { + this.entityService.getEntity(EntityType.FIRMWARE, firmwareId, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.firmwareFormGroup.get('firmwareId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + } else { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.firmwareFormGroup.get('firmwareId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayFirmwareFn(firmware?: FirmwareInfo): string | undefined { + return firmware ? `${firmware.title} (${firmware.version})` : undefined; + } + + fetchFirmware(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'title', + direction: Direction.ASC + }); + return this.firmwareService.getFirmwares(pageLink, true, {ignoreLoading: true}).pipe( + map((data) => data && data.data.length ? data.data : null) + ); + } + + clear() { + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.firmwareInput.nativeElement.blur(); + this.firmwareInput.nativeElement.focus(); + }, 0); + } + + get placeholderText(): string { + return this.labelText || 'firmware.firmware'; + } + + get requiredErrorText(): string { + return this.requiredText || 'firmware.firmware-required'; + } + + firmwareTitleText(firmware: FirmwareInfo): string { + return `${firmware.title} (${firmware.version})`; + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 950ee285e5..ab707b6b07 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -134,6 +134,7 @@ import { DashboardStateDialogComponent } from '@home/components/dashboard-page/s import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; +import { FirmwareAutocompleteComponent } from '@home/components/firmware/firmware-autocomplete.component'; @NgModule({ declarations: @@ -247,7 +248,8 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag ManageDashboardStatesDialogComponent, DashboardStateDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + FirmwareAutocompleteComponent ], imports: [ CommonModule, @@ -351,7 +353,8 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag ManageDashboardStatesDialogComponent, DashboardStateDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + FirmwareAutocompleteComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 23219f1074..a4f9fc3482 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -60,6 +60,10 @@ {{ 'device-profile.type-required' | translate }} + + device-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index a13d15492c..83f248db92 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -48,7 +48,7 @@ import { MatHorizontalStepper } from '@angular/material/stepper'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { deepTrim } from '@core/utils'; -import {ServiceType} from "@shared/models/queue.models"; +import { ServiceType } from '@shared/models/queue.models'; export interface AddDeviceProfileDialogData { deviceProfileName: string; @@ -72,13 +72,13 @@ export class AddDeviceProfileDialogComponent extends entityType = EntityType; - deviceProfileTypes = Object.keys(DeviceProfileType); + deviceProfileTypes = Object.values(DeviceProfileType); deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; deviceTransportTypeHints = deviceTransportTypeHintMap; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -108,6 +108,7 @@ export class AddDeviceProfileDialogComponent extends type: [DeviceProfileType.DEFAULT, [Validators.required]], defaultRuleChainId: [null, []], defaultQueueName: ['', []], + firmwareId: [null], description: ['', []] } ); @@ -186,6 +187,7 @@ export class AddDeviceProfileDialogComponent extends transportType: this.transportConfigFormGroup.get('transportType').value, provisionType: deviceProvisionConfiguration.type, provisionDeviceKey, + firmwareId: this.deviceProfileDetailsFormGroup.get('firmwareId').value, description: this.deviceProfileDetailsFormGroup.get('description').value, profileData: { configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 9ee6201db6..5f7593a03b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -63,6 +63,10 @@ [queueType]="serviceType" formControlName="defaultQueueName"> + + device-profile.type diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index 28ac93c2af..23f1bd511c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -53,11 +53,11 @@ export class DeviceProfileComponent extends EntityComponent { entityType = EntityType; - deviceProfileTypes = Object.keys(DeviceProfileType); + deviceProfileTypes = Object.values(DeviceProfileType); deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -109,6 +109,7 @@ export class DeviceProfileComponent extends EntityComponent { }), defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], defaultQueueName: [entity ? entity.defaultQueueName : '', []], + firmwareId: [entity ? entity.firmwareId : null], description: [entity ? entity.description : '', []], } ); @@ -184,6 +185,7 @@ export class DeviceProfileComponent extends EntityComponent { }}, {emitEvent: false}); this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false}); this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false}); + this.entityForm.patchValue({firmwareId: entity.firmwareId}, {emitEvent: false}); this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 245cb6503b..416b006499 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -48,6 +48,10 @@ device.label + + device-profile.transport-type diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 1cba31aa24..df925e4e21 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -70,7 +70,7 @@ export class DeviceWizardDialogComponent extends entityType = EntityType; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -107,6 +107,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup = this.fb.group({ name: ['', Validators.required], label: [''], + firmwareId: [null], gateway: [false], overwriteActivityTime: [false], transportType: [DeviceTransportType.DEFAULT, Validators.required], @@ -312,6 +313,7 @@ export class DeviceWizardDialogComponent extends const device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, + firmwareId: this.deviceWizardFormGroup.get('firmwareId').value, deviceProfileId: profileId, additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index b01aced285..869b6a0c0a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -95,6 +95,10 @@ device.label + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 261bb1eba5..699bfd2b4e 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -79,6 +79,7 @@ export class DeviceComponent extends EntityComponent { { name: [entity ? entity.name : '', [Validators.required]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], + firmwareId: [entity ? entity.firmwareId : null], label: [entity ? entity.label : ''], deviceData: [entity ? entity.deviceData : null, [Validators.required]], additionalInfo: this.fb.group( @@ -95,6 +96,7 @@ export class DeviceComponent extends EntityComponent { updateForm(entity: DeviceInfo) { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({deviceProfileId: entity.deviceProfileId}); + this.entityForm.patchValue({firmwareId: entity.firmwareId}); this.entityForm.patchValue({label: entity.label}); this.entityForm.patchValue({deviceData: entity.deviceData}); this.entityForm.patchValue({ diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html index 6f43e4a4cc..c85ac61f0b 100644 --- a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html @@ -28,6 +28,16 @@ [fxShow]="!hideDelete() && !isEdit"> {{'resource.delete' | translate }} +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts index e1684c3031..78d04ecf53 100644 --- a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts @@ -24,6 +24,7 @@ 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'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ selector: 'tb-firmware', @@ -109,4 +110,15 @@ export class FirmwaresComponent extends EntityComponent implements OnI } }); } + + onFirmwareIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('firmware.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 72b04828c5..7efac7feb0 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -27,6 +27,7 @@ import { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { FirmwareId } from '@shared/models/id/firmware-id'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -445,6 +446,7 @@ export interface DeviceProfile extends BaseData { provisionDeviceKey?: string; defaultRuleChainId?: RuleChainId; defaultQueueName?: string; + firmwareId?: FirmwareId; profileData: DeviceProfileData; } @@ -499,6 +501,7 @@ export interface Device extends BaseData { name: string; type: string; label: string; + firmwareId?: FirmwareId; deviceProfileId?: DeviceProfileId; deviceData?: DeviceData; additionalInfo?: any; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3cddae95fa..9012be0a99 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1698,6 +1698,7 @@ "checksum": "Checksum", "checksum-required": "Checksum is required.", "checksum-algorithm": "Checksum algorithm", + "copyId": "Copy firmware Id", "description": "Description", "delete": "Delete firmware", "delete-firmware-text": "Be careful, after the confirmation the firmware will become unrecoverable.", @@ -1708,10 +1709,12 @@ "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.", + "idCopiedMessage": "Firmware Id has been copied to clipboard", + "no-firmware-matching": "No firmware matching '{{entity}}' were found.", "no-firmware-text": "No firmwares found", "firmware": "Firmware", "firmware-details": "Firmware details", + "firmware-required": "Firmware is required.", "search": "Search firmwares", "selected-firmware": "{ count, plural, 1 {1 firmware} other {# firmwares} } selected", "title": "Title",