UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 125 kB
{"version":3,"file":"c8y-ngx-components-repository-firmware.mjs","sources":["../../repository/firmware/device-tab/firmware-device-tab.component.ts","../../repository/firmware/device-tab/firmware-device-tab.component.html","../../repository/firmware/device-tab/firmware-device-tab.guard.ts","../../repository/firmware/device-tab/firmware-repository-device-tab.module.ts","../../repository/firmware/list/add-firmware-patch-modal.component.ts","../../repository/firmware/list/add-firmware-patch-modal.component.html","../../repository/firmware/list/add-firmware-modal.component.ts","../../repository/firmware/list/add-firmware-modal.component.html","../../repository/firmware/list/firmware-details.component.ts","../../repository/firmware/list/firmware-details.component.html","../../repository/firmware/list/firmware-list.component.ts","../../repository/firmware/list/firmware-list.component.html","../../repository/firmware/list/firmware-repository-navigation-factory.ts","../../repository/firmware/list/firmware-repository-list.module.ts","../../repository/firmware/firmware-repository.module.ts","../../repository/firmware/c8y-ngx-components-repository-firmware.ts"],"sourcesContent":["import { Component } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { IManagedObject, InventoryService, IOperation, OperationStatus } from '@c8y/client';\nimport { GainsightService, gettext, ModalSelectionMode } from '@c8y/ngx-components';\nimport {\n DeviceFirmware,\n FilterCriteria,\n FirmwareBinary,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositorySelectModalComponent,\n RepositoryService,\n RepositoryType\n} from '@c8y/ngx-components/repository/shared';\nimport { assign, get, isEmpty } from 'lodash-es';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, combineLatest, from, Observable, of } from 'rxjs';\nimport { distinctUntilChanged, filter, map, shareReplay, switchMap, take } from 'rxjs/operators';\n\n@Component({\n selector: 'c8y-firmware-device-tab',\n templateUrl: 'firmware-device-tab.component.html',\n standalone: false\n})\nexport class FirmwareDeviceTabComponent {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED;\n isEmpty = isEmpty;\n reloading = false;\n device$: BehaviorSubject<IManagedObject> = new BehaviorSubject(\n this.route.parent.snapshot.data.contextData\n );\n deviceFirmwareFragment$: Observable<DeviceFirmware> = this.device$.pipe(\n map(device => device.c8y_Firmware)\n );\n firmwareBinary$: Observable<IManagedObject> = this.deviceFirmwareFragment$.pipe(\n filter(deviceFirmwareFragment => !isEmpty(deviceFirmwareFragment)),\n switchMap(deviceFirmwareFragment =>\n from(\n this.repository.getRepositoryBinaryMoByVersion(\n deviceFirmwareFragment,\n RepositoryType.FIRMWARE\n )\n )\n ),\n shareReplay(1)\n );\n repositoryEntry$: Observable<IManagedObject> = this.firmwareBinary$.pipe(\n switchMap(mo => this.repository.getRepositoryEntryMO$(mo)),\n shareReplay(1)\n );\n patches$: Observable<IManagedObject[]> = combineLatest(\n this.firmwareBinary$,\n this.repositoryEntry$\n ).pipe(\n switchMap(([firmwareBinary, repositoryEntry]) => {\n if (repositoryEntry && firmwareBinary) {\n const version: string = this.repository.getBaseVersionFromMO(\n firmwareBinary as FirmwareBinary\n );\n\n return from(this.repository.listPatchVersions(repositoryEntry, version)).pipe(\n map(({ data }) => data)\n );\n } else {\n return of([]);\n }\n }),\n shareReplay(1)\n );\n supportsFirmwareOperations$: Observable<boolean> = this.device$.pipe(\n map(\n (device: IManagedObject) =>\n get(device, 'c8y_SupportedOperations', []).indexOf('c8y_Firmware') > -1\n )\n );\n changesOperation$ = new BehaviorSubject<IOperation>(null);\n changesInProgress$: Observable<boolean> = this.changesOperation$.pipe(\n map(operation => this.isInProgress(operation))\n );\n\n constructor(\n private route: ActivatedRoute,\n private repository: RepositoryService,\n private inventory: InventoryService,\n private bsModal: BsModalService,\n private gainsightService: GainsightService\n ) {}\n\n async ngOnInit() {\n // TODO check route snapshot, why is not refreshing device.\n // Scenario: missing deviceFirmwareFragment => install new version => switch tabs.\n // Expected: device should be set.\n await this.loadDevice();\n await this.loadOperation();\n }\n\n installFirmware() {\n const initialState: Partial<RepositorySelectModalComponent> & {\n repositoryEntriesWithVersionsFn$: (modal: any) => Observable<IManagedObject[]>;\n } = {\n repositoryEntriesWithVersions$: of([]),\n repositoryEntriesWithVersionsFn$: modal =>\n this.getRepositoryEntriesWithVersions$(modal.content.searchTerm),\n repositoryType: RepositoryType.FIRMWARE,\n title: gettext('Install firmware'),\n subTitle: gettext('Available firmwares matching the device type'),\n icon: 'c8y-firmware',\n mode: ModalSelectionMode.SINGLE,\n labels: { ok: gettext('Install') },\n disableSelected: false\n };\n\n this.deviceFirmwareFragment$\n .pipe(\n take(1),\n switchMap(deviceFirmwareFragment => {\n if (deviceFirmwareFragment) {\n const { name, version } = deviceFirmwareFragment;\n const selected = [{ name, version }];\n assign(initialState, { selected });\n }\n\n const modal = this.bsModal.show(RepositorySelectModalComponent, {\n ignoreBackdropClick: true,\n initialState\n });\n\n if (initialState.repositoryEntriesWithVersionsFn$) {\n modal.content.repositoryEntriesWithVersions$ =\n initialState.repositoryEntriesWithVersionsFn$(modal);\n }\n\n modal.content.load.next();\n\n return modal.content.resultEmitter;\n })\n )\n .subscribe(([selectedFirmware]) => {\n this.handleOperation(selectedFirmware);\n });\n }\n\n getRepositoryEntriesWithVersions$(searchTerm$: BehaviorSubject<FilterCriteria>) {\n return searchTerm$.pipe(\n distinctUntilChanged(),\n switchMap(searchTerm =>\n this.repository.listRepositoryEntries(RepositoryType.FIRMWARE, {\n query: this.repository.getDeviceTypeQuery(RepositoryType.FIRMWARE, this.device$.value),\n partialName: searchTerm?.name,\n params: { pageSize: 100 }\n })\n ),\n map(({ data }) => data),\n map(mos => this.getAndAssignRepositoryBinaries(mos)),\n shareReplay(1)\n );\n }\n\n getAndAssignRepositoryBinaries(mos: IManagedObject[]) {\n mos.forEach(mo => {\n mo.versions = this.repository.listBaseVersions(mo);\n });\n return mos;\n }\n\n addPatch() {\n const initialState: Partial<RepositorySelectModalComponent> & {\n repositoryEntriesWithVersions$: Observable<IManagedObject[]>;\n } = {\n repositoryType: RepositoryType.FIRMWARE,\n repositoryEntriesWithVersions$: this.getRepositoryEntryWithPatches$(),\n title: gettext('Install firmware'),\n subTitle: gettext('Available firmwares matching the device type'),\n icon: 'c8y-firmware',\n mode: ModalSelectionMode.SINGLE,\n labels: { ok: gettext('Install') },\n disableSelected: false\n };\n\n this.deviceFirmwareFragment$\n .pipe(\n take(1),\n switchMap(deviceFirmwareFragment => {\n if (deviceFirmwareFragment) {\n const { name, version } = deviceFirmwareFragment;\n const selected = [{ name, version }];\n assign(initialState, { selected });\n }\n\n const modal = this.bsModal.show(RepositorySelectModalComponent, {\n ignoreBackdropClick: true,\n initialState\n });\n modal.content.load.next();\n\n return modal.content.resultEmitter;\n })\n )\n .subscribe(([selectedOption]) => {\n this.handleOperation(selectedOption);\n });\n }\n\n getRepositoryEntryWithPatches$() {\n return combineLatest(this.repositoryEntry$, this.patches$).pipe(\n map(([repositoryEntry, patches]) => {\n return [{ ...repositoryEntry, versions: patches }];\n })\n );\n }\n\n async loadDevice() {\n this.reloading = true;\n const deviceId = this.device$.value.id;\n const device = (await this.inventory.detail(deviceId, { withChildren: false })).data;\n this.device$.next(device);\n this.reloading = false;\n }\n\n private async handleOperation(selectedFirmware) {\n const operation = await this.repository.createFirmwareUpdateOperation(\n this.device$.value,\n selectedFirmware\n );\n this.trackOperation(operation);\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.EVENTS.DEVICE_TAB,\n {\n component: PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.COMPONENTS.FIRMWARE_DEVICE_TAB,\n result:\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.RESULTS.CREATE_FIRMWARE_UPDATE_OPERATION\n }\n );\n }\n\n private async loadOperation() {\n const deviceId = this.device$.value.id;\n const operation = await this.repository.getLastFirmwareUpdateOperation(deviceId);\n this.trackOperation(operation);\n }\n\n private trackOperation(operation: IOperation) {\n if ([OperationStatus.SUCCESSFUL, OperationStatus.FAILED].includes(operation?.status)) {\n this.changesOperation$.next(undefined);\n } else this.changesOperation$.next(operation);\n\n if (this.isInProgress(operation)) {\n this.repository.observeOperation(operation).subscribe(\n operationUpdate => {\n this.changesOperation$.next(operationUpdate);\n if (operationUpdate.status === OperationStatus.SUCCESSFUL) {\n this.loadDevice();\n }\n },\n operationUpdate => {\n this.changesOperation$.next(operationUpdate);\n }\n );\n }\n }\n\n private isInProgress(operation: IOperation) {\n return (\n operation && [OperationStatus.PENDING, OperationStatus.EXECUTING].includes(operation.status)\n );\n }\n}\n","<div class=\"row\">\n <div class=\"col-lg-12 col-lg-max\">\n <div class=\"card split-view--7-5 m-b-0\">\n <div class=\"d-flex d-col flex-grow split-view__list\">\n <div class=\"card-header separator\">\n <div\n class=\"card-title\"\n translate\n >\n Current firmware\n </div>\n </div>\n <div class=\"inner-scroll\">\n <div class=\"card-block p-t-0 p-b-0\">\n <!-- EMPTY STATE -->\n <ng-container *ngIf=\"isEmpty(deviceFirmwareFragment$ | async); else firmwareBlock\">\n <div class=\"c8y-empty-state text-center\">\n <div\n class=\"h1 c8y-icon-duocolor\"\n c8yIcon=\"c8y-firmware\"\n ></div>\n <p>\n <strong translate>No firmware installed.</strong>\n <br />\n <small translate>Click below to install firmware into this device.</small>\n </p>\n </div>\n </ng-container>\n\n <!-- FIRMWARE -->\n <ng-template #firmwareBlock>\n <c8y-list-group class=\"no-border-last\">\n <c8y-li>\n <c8y-li-icon>\n <i c8yIcon=\"c8y-firmware\"></i>\n </c8y-li-icon>\n\n <c8y-li-body *ngIf=\"deviceFirmwareFragment$ | async as deviceFirmwareFragment\">\n <!-- Firmware title -->\n <p class=\"text-medium\">\n {{ deviceFirmwareFragment.name }}\n </p>\n <!-- Firmware description -->\n <div *ngIf=\"repositoryEntry$ | async as repositoryEntry\">\n <span\n class=\"text-label-small m-r-4\"\n translate\n >\n Description\n </span>\n <span>\n {{ repositoryEntry.description }}\n </span>\n </div>\n\n <!-- BASE/PATCH VERSION -->\n <div class=\"d-flex a-i-baseline\">\n <p\n class=\"text-label-small m-r-4\"\n translate\n >\n Version\n </p>\n <p *ngIf=\"deviceFirmwareFragment.version; else versionNotSpecified\">\n {{ deviceFirmwareFragment.version }}\n </p>\n <ng-template #versionNotSpecified>\n <p>\n <em class=\"text-muted\">({{ 'not specified`version`' | translate }})</em>\n </p>\n </ng-template>\n </div>\n\n <!-- ADD PATCH -->\n <button\n class=\"btn btn-xs btn-primary\"\n title=\"{{ 'Patches available' | translate }}\"\n *ngIf=\"\n (supportsFirmwareOperations$ | async) && (this.patches$ | async)?.length > 0\n \"\n (click)=\"addPatch()\"\n [disabled]=\"changesInProgress$ | async\"\n c8yProductExperience\n [actionName]=\"PRODUCT_EXPERIENCE.FIRMWARE.EVENTS.DEVICE_TAB\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.FIRMWARE.COMPONENTS.FIRMWARE_DEVICE_TAB,\n action:\n PRODUCT_EXPERIENCE.FIRMWARE.ACTIONS.OPEN_INSTALL_FIRMWARE_PATCH_DIALOG\n }\"\n >\n {{ 'Patches available' | translate }}\n </button>\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n </ng-template>\n </div>\n </div>\n <div\n class=\"card-footer separator-top\"\n *ngIf=\"supportsFirmwareOperations$ | async\"\n >\n <!-- INSTALL FIRMWARE -->\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Install firmware' | translate }}\"\n *ngIf=\"isEmpty(deviceFirmwareFragment$ | async)\"\n (click)=\"installFirmware()\"\n c8yProductExperience\n [actionName]=\"PRODUCT_EXPERIENCE.FIRMWARE.EVENTS.DEVICE_TAB\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.FIRMWARE.COMPONENTS.FIRMWARE_DEVICE_TAB,\n action: PRODUCT_EXPERIENCE.FIRMWARE.ACTIONS.OPEN_INSTALL_FIRMWARE_DIALOG\n }\"\n >\n {{ 'Install firmware' | translate }}\n </button>\n\n <!-- REPLACE FIRMWARE -->\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Replace firmware' | translate }}\"\n *ngIf=\"!isEmpty(deviceFirmwareFragment$ | async)\"\n (click)=\"installFirmware()\"\n [disabled]=\"changesInProgress$ | async\"\n c8yProductExperience\n [actionName]=\"PRODUCT_EXPERIENCE.FIRMWARE.EVENTS.DEVICE_TAB\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.FIRMWARE.COMPONENTS.FIRMWARE_DEVICE_TAB,\n action: PRODUCT_EXPERIENCE.FIRMWARE.ACTIONS.OPEN_REPLACE_FIRMWARE_DIALOG\n }\"\n >\n {{ 'Replace firmware' | translate }}\n </button>\n </div>\n </div>\n <div class=\"inner-scroll d-flex d-col bg-level-1 split-view__detail\">\n <div class=\"card-header separator large-padding sticky-top\">\n <div\n class=\"card-title\"\n translate\n >\n Firmware changes\n </div>\n </div>\n <div class=\"flex-grow\">\n <fieldset\n class=\"card-block large-padding bg-level-2 p-0\"\n id=\"operation-block\"\n *ngIf=\"changesOperation$ | async\"\n >\n <c8y-operation-details [operation]=\"changesOperation$ | async\"></c8y-operation-details>\n </fieldset>\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Injectable } from '@angular/core';\n\nimport { get, has, indexOf } from 'lodash-es';\n\nconst FIRMWARE_FRAGMENT = 'c8y_Firmware';\nconst SUPPORTED_OPERATIONS_FRAGMENT = 'c8y_SupportedOperations';\n\n@Injectable()\nexport class FirmwareDeviceTabGuard {\n canActivate(route) {\n const contextData = get(route, 'data.contextData') || get(route, 'parent.data.contextData');\n const supportedOperations = get(contextData, SUPPORTED_OPERATIONS_FRAGMENT);\n return (\n (!!supportedOperations ? indexOf(supportedOperations, FIRMWARE_FRAGMENT) >= 0 : false) ||\n has(contextData, 'c8y_Firmware')\n );\n }\n}\n","import { ModuleWithProviders, NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FirmwareDeviceTabComponent } from './firmware-device-tab.component';\nimport { FirmwareDeviceTabGuard } from './firmware-device-tab.guard';\nimport { CoreModule, gettext, hookRoute, ViewContext } from '@c8y/ngx-components';\nimport { SharedRepositoryModule } from '@c8y/ngx-components/repository/shared';\nimport { OperationDetailsModule } from '@c8y/ngx-components/operations/operation-details';\n\n@NgModule({\n imports: [CommonModule, CoreModule, SharedRepositoryModule, OperationDetailsModule],\n declarations: [FirmwareDeviceTabComponent]\n})\nexport class FirmwareRepositoryDeviceTabModule {\n static forRoot(): ModuleWithProviders<FirmwareRepositoryDeviceTabModule> {\n return {\n ngModule: FirmwareRepositoryDeviceTabModule,\n providers: [\n FirmwareDeviceTabGuard,\n hookRoute({\n context: ViewContext.Device,\n path: 'firmware',\n component: FirmwareDeviceTabComponent,\n label: gettext('Firmware'),\n icon: 'c8y-firmware',\n priority: 500,\n canActivate: [FirmwareDeviceTabGuard]\n })\n ]\n };\n }\n}\n","import { Component, EventEmitter, Output, ViewChild } from '@angular/core';\nimport { NgForm } from '@angular/forms';\nimport { IManagedObject, IResultList } from '@c8y/client';\nimport { AlertService, gettext, PickedFiles } from '@c8y/ngx-components';\nimport {\n ModalModel,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryCategory,\n RepositoryService,\n RepositoryType\n} from '@c8y/ngx-components/repository/shared';\nimport { isUndefined } from 'lodash-es';\nimport { BsDropdownDirective } from 'ngx-bootstrap/dropdown';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, from, merge, Observable, of, pipe } from 'rxjs';\nimport {\n debounceTime,\n distinctUntilChanged,\n map,\n shareReplay,\n switchMap,\n tap\n} from 'rxjs/operators';\n\n@Component({\n selector: 'c8y-add-firmware-patch-modal.component',\n templateUrl: 'add-firmware-patch-modal.component.html',\n standalone: false\n})\nexport class AddFirmwarePatchModalComponent {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED;\n @Output() saved: EventEmitter<RepositoryCategory> = new EventEmitter<RepositoryCategory>();\n\n @ViewChild('dropdown', { static: false }) dropdown: BsDropdownDirective;\n @ViewChild('firmwarePatchForm', { static: false }) form: NgForm;\n textForFirmwareUrlPopover: string =\n gettext(`Path for binaries can vary depending on device agent implementation, for example:\n /firmware/binaries/firmware1.bin\n https://firmware/binary/123\n ftp://firmware/binary/123.tar.gz\n `);\n\n model: ModalModel = {\n selected: undefined,\n dependency: null,\n patchVersion: undefined,\n binary: {\n file: undefined,\n url: undefined\n }\n };\n\n firmwareInput$ = new BehaviorSubject<string>('');\n firmwares$: Observable<IResultList<IManagedObject>> = this.firmwareInput$.pipe(\n debounceTime(300),\n distinctUntilChanged(),\n switchMap(searchStr =>\n from(\n this.repository.listRepositoryEntries(RepositoryType.FIRMWARE, {\n partialName: searchStr,\n skipLegacy: true\n })\n )\n ),\n shareReplay(1)\n );\n firmwareSelected$ = new BehaviorSubject<Partial<RepositoryCategory>>(null);\n patchDependencyInput$ = new BehaviorSubject<string>('');\n\n saving = false;\n firmwarePreselected = false;\n baseVersions$: Observable<IResultList<IManagedObject>> = merge(\n this.firmwareInput$.pipe(\n tap(() => {\n this.model.dependency = null;\n if (this.form) {\n this.form.form.get('patchDependency').reset();\n }\n }),\n switchMap(() => of(null))\n ),\n this.firmwareSelected$\n ).pipe(\n switchMap(selectedFirmware =>\n selectedFirmware ? this.repository.listBaseVersions(selectedFirmware) : of(null)\n ),\n shareReplay(1)\n );\n baseVersionsFilterPipe = pipe(\n switchMap((data: []) =>\n this.patchDependencyInput$.pipe(\n map(partialVersion =>\n data.filter((mo: any) => {\n const version = mo.c8y_Firmware.version?.toLowerCase();\n return (\n partialVersion.length === 0 || version?.indexOf(partialVersion.toLowerCase()) > -1\n );\n })\n )\n )\n )\n );\n\n constructor(\n private modal: BsModalRef,\n private repository: RepositoryService,\n private alert: AlertService\n ) {}\n\n async ngOnInit() {\n this.setInitialState();\n }\n\n setInitialState() {\n if (this.model.selected) {\n this.firmwarePreselected = true;\n this.firmwareSelected$.next(this.model.selected);\n }\n }\n\n async save() {\n this.saving = true;\n this.repository\n .create(this.model, RepositoryType.FIRMWARE)\n .then(savedFirmware => {\n this.successMsg();\n this.saving = false;\n this.saved.next(savedFirmware);\n this.cancel();\n })\n .catch(e => {\n this.saving = false;\n this.saved.error(e);\n this.cancel();\n });\n }\n\n successMsg() {\n const msg = gettext('Firmware patch added.');\n this.alert.success(msg);\n }\n\n cancel() {\n this.modal.hide();\n this.saved.complete();\n }\n\n onFile(dropped: PickedFiles) {\n if (!isUndefined(dropped.url)) {\n this.model.binary = {\n url: dropped.url\n };\n return;\n } else if (dropped.droppedFiles) {\n this.model.binary = {\n file: dropped.droppedFiles[0].file\n };\n return;\n } else {\n this.model.binary = {\n file: undefined,\n url: undefined\n };\n }\n }\n}\n","<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'c8y-firmware'\"></i>\n <h4 translate id=\"addFirmwarePatchModalTitle\">Add firmware patch</h4>\n </div>\n <div class=\"p-16 text-center separator-bottom\" id=\"addFirmwarePatchModalDescription\">\n <p class=\"text-medium text-16 m-0\" translate>Select a firmware version</p>\n </div>\n\n <form\n class=\"d-contents\"\n autocomplete=\"off\"\n #firmwarePatchForm=\"ngForm\"\n (ngSubmit)=\"firmwarePatchForm.form.valid && save()\"\n >\n <div class=\"modal-inner-scroll\">\n <div class=\"modal-body\">\n <div [hidden]=\"firmwarePreselected\">\n <c8y-form-group>\n <label for=\"firmwareName\" translate>Firmware</label>\n <c8y-typeahead\n [ngModel]=\"model.selected\"\n name=\"firmwareName\"\n placeholder=\"{{ 'Select or enter' | translate }}\"\n (onSearch)=\"firmwareInput$.next($event)\"\n [allowFreeEntries]=\"false\"\n [required]=\"true\"\n >\n <c8y-li\n *c8yFor=\"let firmware of firmwares$ | async; loadMore: 'auto'\"\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n (click)=\"model.selected = firmware; firmwareSelected$.next(firmware)\"\n [active]=\"model.selected === firmware\"\n >\n <c8y-highlight\n [text]=\"firmware.name || '--'\"\n [pattern]=\"firmwareInput$ | async\"\n ></c8y-highlight>\n </c8y-li>\n </c8y-typeahead>\n <c8y-messages>\n <c8y-message\n name=\"notExisting\"\n [text]=\"'Select one of the existing firmwares.' | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n </div>\n\n <c8y-form-group>\n <label for=\"patchDependency\" class=\"m-r-8\" translate>Version</label>\n <c8y-typeahead\n [ngModel]=\"model.dependency\"\n name=\"patchDependency\"\n data-cy=\"add-firmware-patch-modal--patchDependency\"\n placeholder=\"{{ 'Select or enter' | translate }}\"\n (onSearch)=\"patchDependencyInput$.next($event)\"\n [displayProperty]=\"'c8y_Firmware.version'\"\n [allowFreeEntries]=\"false\"\n [disabled]=\"\n (baseVersions$ | async) === null || (baseVersions$ | async)?.data.length === 0\n \"\n [required]=\"true\"\n >\n <c8y-li\n *c8yFor=\"\n let baseVersion of baseVersions$;\n loadMore: 'auto';\n pipe: baseVersionsFilterPipe\n \"\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n (click)=\"model.dependency = baseVersion\"\n [active]=\"model.dependency === baseVersion\"\n >\n <c8y-highlight\n [text]=\"baseVersion.c8y_Firmware.version || '--'\"\n [pattern]=\"patchDependencyInput$ | async\"\n ></c8y-highlight>\n </c8y-li>\n </c8y-typeahead>\n <c8y-messages>\n <c8y-message\n name=\"notExisting\"\n [text]=\"'Select one of the existing versions.' | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n\n <c8y-form-group>\n <label for=\"patchVersion\" translate>Patch</label>\n <input\n id=\"patchVersion\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"patchVersion\"\n data-cy=\"add-firmware-patch-modal--patchVersion\"\n [(ngModel)]=\"model.patchVersion\"\n placeholder=\"{{ 'e.g.' | translate }} 1.0.0\"\n required\n />\n </c8y-form-group>\n\n <c8y-form-group>\n <div class=\"legend form-block m-t-40\" translate>Patch file</div>\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n (onFilesPicked)=\"onFile($event)\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'provided']\"\n [fileUrlPopover]=\"textForFirmwareUrlPopover\"\n ></c8y-file-picker>\n </c8y-form-group>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n title=\"{{ 'Cancel' | translate }}\"\n data-cy=\"add-firmware-patch-modal--cancel-btn\"\n class=\"btn btn-default\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving\"\n translate\n >\n Cancel\n </button>\n <button\n title=\"{{ 'Add firmware patch' | translate }}\"\n class=\"btn btn-primary\"\n type=\"submit\"\n [ngClass]=\"{ 'btn-pending': saving }\"\n [disabled]=\"\n !firmwarePatchForm.form.valid ||\n firmwarePatchForm.form.pristine ||\n (!model.binary?.url && !model.binary?.file && model.binary?.url !== '$PROVIDED') ||\n saving\n \"\n translate\n c8yProductExperience\n [actionName]=\"PRODUCT_EXPERIENCE.FIRMWARE.EVENTS.REPOSITORY\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.FIRMWARE.COMPONENTS.ADD_FIRMWAR_PATCH_MODAL,\n result: PRODUCT_EXPERIENCE.FIRMWARE.RESULTS.ADD_FIRMWARE_PATCH\n }\"\n >\n Add firmware patch\n </button>\n </div>\n </form>\n</div>\n","import { Component, EventEmitter, Output, ViewChild } from '@angular/core';\nimport { NgForm } from '@angular/forms';\nimport { IManagedObject } from '@c8y/client';\nimport { AlertService, gettext, PickedFiles, ValidationPattern } from '@c8y/ngx-components';\nimport {\n ModalModel,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryCategory,\n RepositoryService,\n RepositoryType\n} from '@c8y/ngx-components/repository/shared';\nimport { assign, get, isUndefined } from 'lodash-es';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, from, Subscription } from 'rxjs';\nimport { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';\n\n@Component({\n selector: 'c8y-add-firmware-software-modal',\n templateUrl: 'add-firmware-modal.component.html',\n standalone: false\n})\nexport class AddFirmwareModalComponent {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED;\n @ViewChild('firmwareForm', { static: false }) form: NgForm;\n @Output() saved: EventEmitter<RepositoryCategory> = new EventEmitter<RepositoryCategory>();\n onInput: BehaviorSubject<string> = new BehaviorSubject<string>('');\n model: ModalModel = {\n selected: undefined,\n version: undefined,\n description: undefined,\n deviceType: undefined,\n binary: {\n file: undefined,\n url: undefined\n }\n };\n firmwaresResult;\n saving = false;\n firmwarePreselected = false;\n textForFirmwareUrlPopover: string =\n gettext(`Path for binaries can vary depending on device agent implementation, for example:\n /firmware/binaries/firmware1.bin\n https://firmware/binary/123\n ftp://firmware/binary/123.tar.gz\n `);\n ValidationPattern = ValidationPattern;\n private inputSubscription$: Subscription;\n\n constructor(\n private modal: BsModalRef,\n private repositoryService: RepositoryService,\n private alert: AlertService\n ) {}\n\n ngOnInit() {\n this.setInitialState();\n this.loadFirmwares();\n }\n\n setInitialState() {\n if (this.model.selected) {\n this.firmwarePreselected = true;\n }\n }\n\n loadFirmwares() {\n this.inputSubscription$ = this.onInput\n .pipe(\n tap(() => {\n if (!this.firmwarePreselected) {\n this.model.description = null;\n if (this.form) {\n this.form.form.get('description').reset();\n }\n }\n }),\n debounceTime(300),\n distinctUntilChanged(),\n switchMap(searchStr => this.getFirmwareResult(searchStr))\n )\n .subscribe(result => {\n this.firmwaresResult = result;\n });\n }\n\n getFirmwareResult(searchStr: string) {\n return from(\n this.repositoryService.listRepositoryEntries(RepositoryType.FIRMWARE, {\n partialName: searchStr,\n skipLegacy: true\n })\n );\n }\n\n async save() {\n this.saving = true;\n this.repositoryService\n .create(this.model, RepositoryType.FIRMWARE)\n .then(savedFirmware => {\n this.successMsg();\n this.saving = false;\n this.saved.next(savedFirmware);\n this.cancel();\n })\n .catch(e => {\n this.saving = false;\n this.saved.error(e);\n this.cancel();\n });\n }\n\n successMsg() {\n const msg = gettext('Firmware added.');\n this.alert.success(msg);\n }\n\n cancel() {\n this.modal.hide();\n this.saved.complete();\n }\n\n ngOnDestroy() {\n this.inputSubscription$.unsubscribe();\n }\n\n onFile(dropped: PickedFiles) {\n if (!isUndefined(dropped.url)) {\n this.model.binary = {\n url: dropped.url\n };\n return;\n } else if (dropped.droppedFiles) {\n this.model.binary = {\n file: dropped.droppedFiles[0].file\n };\n return;\n } else {\n this.model.binary = {\n file: undefined,\n url: undefined\n };\n }\n }\n\n onFirmwareSelect(firmware: IManagedObject) {\n assign(this.model, {\n selected: firmware,\n description: firmware.description,\n deviceType: get(firmware, 'c8y_Filter.type')\n });\n }\n}\n","<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'c8y-firmware'\"></i>\n <div class=\"modal-title\" translate id=\"addFirmwareModalTitle\">Add firmware</div>\n </div>\n <div class=\"p-16 text-center separator-bottom\" *ngIf=\"!firmwarePreselected\">\n <p class=\"text-medium text-16 m-0\" translate>Select or create new firmware</p>\n </div>\n <form\n class=\"d-contents\"\n autocomplete=\"off\"\n #firmwareForm=\"ngForm\"\n (ngSubmit)=\"firmwareForm.form.valid && save()\"\n >\n <div class=\"modal-inner-scroll\">\n <div class=\"modal-body\" id=\"addFirmwareModalDescription\">\n <div [hidden]=\"firmwarePreselected\">\n <c8y-form-group>\n <label for=\"firmwareName\" translate>Firmware</label>\n <c8y-typeahead\n [(ngModel)]=\"model.selected\"\n name=\"firmwareName\"\n placeholder=\"{{ 'Select or enter' | translate }}\"\n data-cy=\"add-firmware-modal--input-name\"\n (onSearch)=\"onInput.next($event)\"\n [required]=\"true\"\n >\n <c8y-li\n *c8yFor=\"\n let firmware of firmwaresResult;\n loadMore: 'auto';\n notFound: notFoundTemplate\n \"\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n (click)=\"onFirmwareSelect(firmware)\"\n [active]=\"model.selected === firmware\"\n >\n <c8y-highlight\n [text]=\"firmware.name || '--'\"\n [pattern]=\"onInput | async\"\n ></c8y-highlight>\n </c8y-li>\n <ng-template #notFoundTemplate>\n <c8y-li class=\"bg-level-2 p-8\" *ngIf=\"(onInput | async)?.length > 0\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n type=\"button\"\n title=\"{{ 'Add new`firmware`' | translate }}\"\n translate\n >\n Add new`firmware`\n </button>\n </c8y-li>\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n\n <c8y-form-group>\n <label for=\"firmwareDescription\" translate>Description</label>\n <input\n id=\"firmwareDescription\"\n data-cy=\"add-firmware-modal--input-description\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"model.description\"\n placeholder=\"{{ 'e.g. Firmware for hardware revision B' | translate }}\"\n [disabled]=\"model.selected?.id\"\n [required]=\"true\"\n [pattern]=\"ValidationPattern.rules.noWhiteSpaceOnly.pattern\"\n />\n </c8y-form-group>\n\n <c8y-form-group>\n <label class=\"control-label\" for=\"firmwareDeviceTypeFilter\">\n {{ 'Device type filter' | translate }}\n <button\n class=\"btn-help\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{\n 'If the filter is set, the firmware will show up for installation only for devices of that type. If no filter is set, it will be available for all devices.'\n | translate\n }}\"\n triggers=\"focus\"\n placement=\"right\"\n container=\"body\"\n ></button>\n </label>\n <input\n id=\"firmwareDeviceTypeFilter\"\n data-cy=\"add-firmware-modal--firmwareDeviceTypeFilter\"\n class=\"form-control\"\n name=\"firmwareDeviceTypeFilter\"\n [(ngModel)]=\"model.deviceType\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n [disabled]=\"model.selected?.id\"\n [pattern]=\"ValidationPattern.rules.noWhiteSpaceOnly.pattern\"\n />\n </c8y-form-group>\n </div>\n\n <c8y-form-group>\n <label for=\"firmwareVersion\" translate>Version</label>\n <input\n id=\"firmwareVersion\"\n data-cy=\"add-firmware-modal--firmwareVersion\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"version\"\n [(ngModel)]=\"model.version\"\n placeholder=\"{{ 'e.g.' | translate }} 1.0.0\"\n [required]=\"true\"\n [pattern]=\"ValidationPattern.rules.noWhiteSpaceOnly.pattern\"\n />\n </c8y-form-group>\n\n <c8y-form-group>\n <div class=\"legend form-block m-t-40\" translate>Firmware file</div>\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n (onFilesPicked)=\"onFile($event)\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'provided']\"\n [fileUrlPopover]=\"textForFirmwareUrlPopover\"\n ></c8y-file-picker>\n </c8y-form-group>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n type=\"button\"\n title=\"{{ 'Cancel' | translate }}\"\n data-cy=\"add-firmware-modal--cancel-btn\"\n (click)=\"cancel()\"\n [disabled]=\"saving\"\n translate\n >\n Cancel\n </button>\n\n <button\n class=\"btn btn-primary\"\n type=\"submit\"\n title=\"{{ 'Add firmware' | translate }}\"\n [ngClass]=\"{ 'btn-pending': saving }\"\n [disabled]=\"\n !firmwareForm.form.valid ||\n firmwareForm.form.pristine ||\n saving ||\n (!model.binary?.url && !model.binary?.file)\n \"\n [actionName]=\"PRODUCT_EXPERIENCE.FIRMWARE.EVENTS.REPOSITORY\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.FIRMWARE.COMPONENTS.ADD_FIRMWARE_MODAL,\n result:\n firmwarePreselected || model.selected?.id\n ? PRODUCT_EXPERIENCE.FIRMWARE.RESULTS.ADD_FIRMWARE_VERSION\n : PRODUCT_EXPERIENCE.FIRMWARE.RESULTS.ADD_FIRMWARE\n }\"\n translate\n c8yProductExperience\n >\n Add firmware\n </button>\n </div>\n </form>\n</div>\n","import { Component, OnDestroy, OnInit } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { IManagedObject, IResultList, InventoryService } from '@c8y/client';\nimport {\n AlertService,\n GainsightService,\n ModalService,\n Status,\n gettext,\n memoize\n} from '@c8y/ngx-components';\nimport {\n FirmwareBinary,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryService\n} from '@c8y/ngx-components/repository/shared';\nimport { TranslateService } from '@ngx-translate/core';\nimport { property } from 'lodash-es';\nimport { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, Observable, Subject, combineLatest, defer, merge } from 'rxjs';\nimport {\n distinctUntilKeyChanged,\n map,\n shareReplay,\n switchMap,\n take,\n takeUntil,\n tap,\n withLatestFrom\n} from 'rxjs/operators';\nimport { AddFirmwareModalComponent } from './add-firmware-modal.component';\nimport { AddFirmwarePatchModalComponent } from './add-firmware-patch-modal.component';\n\n@Component({\n selector: 'c8y-firmware-details',\n templateUrl: './firmware-details.component.html',\n standalone: false\n})\nexport class FirmwareDetailsComponent implements OnInit, OnDestroy {\n reload$: Subject<void> = new Subject();\n reloading$: BehaviorSubject<boolean> = new BehaviorSubject(false);\n firmware: IManagedObject;\n\n updateFirmware$: Subject<Partial<IManagedObject>> = new Subject();\n firmwareUpdated$: Subject<IManagedObject> = new Subject();\n baseVersionsUpdated$: Subject<void> = new Subject();\n patchVersionsUpdated$: Subject<void> = new Subject();\n\n firmware$: Observable<IManagedObject> = merge(\n this.activatedRoute.params.pipe(\n map(params => params.id),\n switchMap(id => defer(() => this.inventoryService.detail(id).then(result => result.data)))\n ),\n this.reload$.pipe(\n tap(() => this.reloading$.next(true)),\n switchMap(() => this.activatedRoute.params),\n map(params => params.id),\n switchMap(id => defer(() => this.inventoryService.detail(id).then(result => result.data))),\n tap(() => this.reloading$.next(false))\n ),\n this.firmwareUpdated$\n ).pipe(shareReplay(1));\n\n baseVersions$: Observable<IResultList<IManagedObject>> = merge(\n this.firmware$.pipe(distinctUntilKeyChanged('id')),\n this.baseVersionsUpdated$,\n this.patchVersionsUpdated$,\n this.reload$\n ).pipe(\n switchMap(() => this.firmware$),\n switchMap(firmware => this.repositoryService.listBaseVersions(firmware)),\n shareReplay(1)\n );\n\n isLegacy$: Observable<boolean> = this.firmware$.pipe(\n map(firmware => this.repositoryService.isLegacyEntry(firmware)),\n shareReplay(1)\n );\n\n canAddPatchVersions$: Observable<boolean> = combineLatest(\n this.isLegacy$,\n this.baseVersions$.pipe(map(({ data }) => data.length > 0))\n ).pipe(map(([isLegacy, hasBaseVersions]) => !isLegacy && hasBaseVersions));\n\n expanded: { [id: string]: boolean } = {};\n\n destroy$: Subject<boolean> = new Subject<boolean>();\n\n constructor(\n private activatedRoute: ActivatedRoute,\n private inventoryService: InventoryService,\n private repositoryService: RepositoryService,\n private alertService: AlertService,\n private translateService: TranslateService,\n private modalService: ModalService,\n private bsModalService: BsModalService,\n private gainsightService: GainsightService,\n private router: Router\n ) {}\n\n ngOnInit() {\n this.updateFirmware$\n .pipe(\n withLatestFrom(this.firmware$),\n switchMap(([firmwarePartial, firmware]) =>\n this.inventoryService.update({\n id: firmware.id,\n ...firmwarePartial\n })\n ),\n map(({ data }) => data),\n tap(firmware => this.firmwareUpdated$.next(firmware)),\n tap(() =>\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.EVENTS.REPOSITORY,\n {\n result: PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.RESULTS.EDIT_FIRMWARE\n }\n )\n ),\n tap(() => this.alertService.success(gettext('Saved.'))),\n takeUntil(this.destroy$)\n )\n .subscribe();\n\n this.firmware$.subscribe(firmware => {\n this.firmware = firmware;\n });\n }\n\n @memoize(property('id'))\n getPatchVersionsCount$(baseVersion: FirmwareBinary) {\n return merge(\n this.firmware$.pipe(distinctUntilKeyChanged('id')),\n this.baseVersionsUpdated$,\n this.patchVersionsUpdated$,\n this.reload$\n ).pipe(\n switchMap(() => this.firmware$),\n switchMap(firmware => this.repositoryService.getPatchVersionsCount$(firmware, baseVersion)),\n shareReplay(1)\n );\n }\n\n @memoize()\n getBinaryName$(binaryUrl) {\n return this.repositoryService.getBinaryName$(binaryUrl);\n }\n\n @memoize(property('id'))\n getPatchVersions$(baseVersion) {\n return merge(\n this.firmware$.pipe(distinctUntilKeyChanged('id')),\n this.patchVersionsUpdated$,\n this.reload$\n ).pipe(\n switchMap(() => this.firmware$),\n switchMap(firmware => this.repositoryService.listPatchVersions(firmware, baseVersion)),\n shareReplay(1)\n );\n }\n\n addBaseVersion() {\n this.firmware$\n .pipe(\n take(1),\n switchMap(firmware => {\n const initialState = {\n model: {\n selected: firmware,\n description: firmware.description\n }\n };\n const config: ModalOptions<AddFirmwareModalComponent> = {\n class: 'modal-sm',\n ignoreBackdropClick: true,\n keyboard: false,\n ariaDescribedby: 'addFirmwareModalDescription',\n ariaLabelledBy: 'addFirmwareModalTitle',\n initialState\n };\n const modalRef = this.bsModalService.show(AddFirmwareModalComponent, config);\n return modalRef.content.saved;\n })\n )\n .subscribe(() => this.baseVersionsUpdated$.next());\n }\n\n addPatchVersion() {\n this.firmware$\n .pipe(\n take(1),\n switchMap(firmware => {\n const initialState = {\n model: {\n selected: firmware\n }\n };\n const config: ModalOptions<AddFirmwarePatchModalComponent> = {\n class: 'modal-sm',\n ignoreBackdropClick: true,\n keyboard: false,\n ariaDescribedby: 'addFirmwarePatchModalDescription',\n ariaLabelledBy: 'addFirmwarePatchModalTitle',\n initialState\n };\n const modalRef = this.bsModalService.show(AddFirmwarePatchModalComponent, config);\n return modalRef.content.saved;\n })\n )\n .subscribe(() => this.patchVersionsUpdated$.next());\n }\n\n async deleteBaseVersion(baseVersion: IManagedObject) {\n try {\n const title = gettext('Delete firmware');\n const body = `\n ${this.translateService.instant(\n gettext('You are about to delete firmware {{ version }} with all its patches.'),\n { version: baseVersion.c8y_Firmware.version }\n )}\n ${this.translateService.instant(gettext('This operation is irreversible.'))}\n ${this.translateService.instant(gettext('Do you want to proceed?'))}\n `;\n const labels = {\n ok: gettext('Delete')\n };\n await this.modalService.confirm(\n title,\n body,\n Status.DANGER,\n labels,\n {},\n { eventName: PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.EVENTS.REPOSITORY }\n );\n const isLastVersion = await this.baseVersions$\n .pipe(\n map(versions => versions?.data?.length === 1),\n take(1)\n )\n .toPromise();\n if (isLastVersion) {\n await this.repositoryService.delete(this.firmware);\n this.router.navigateByUrl('/firmware');\n } else {\n await this.repositoryService.delete(baseVersion);\n this.baseVersionsUpdated$.next();\n }\n this.alertService.success(gettext('Firmware deleted.'));\n } catch (ex) {\n // only if not cancel from modal\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n async deletePatchVersion(patchVersion: IManagedObject) {\n try {\n const title = gettext('Delete firmware patch');\n const body = `\n ${this.translateService.instant(\n gettext('You are about to delete firmware patch {{ version }}.'),\n { version: patchVersion.c8y_Firmware.version }\n )}\n ${this.translateService.instant(gettext('This operation is irreversible.'))}\n ${this.translateService.instant(gettext('Do you want to proceed?'))}\n `;\n const labels = {\n ok: gettext('Delete')\n };\n await this.modalService.confirm(\n title,\n body,\n Status.DANGER,\n labels,\n {},\n { eventName: PRODUCT_EXPERIENCE_REPOSITORY_SHARED.FIRMWARE.EVENTS.REPOSITORY }\n );\n await this.repositoryService.delete(patchVersion);\n this.alertService.success(gettext('Firmware patch deleted.'));\n this.patchVersionsUpdated$.next();\n } catch (ex) {\n // only if not cancel from modal\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n ngOnDestroy() {\n this.destroy$.next(true);\n this.destroy$.unsubscribe();\n }\n}\n","<c8y-title>\n {{ (firmware$ | async)?.name }}\n</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n icon=\"c8y-management\"\n label=\"{{ 'Management' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n icon=\"c8y-firmware\"\n path=\"#/firmware\"\n label=\"{{ 'Firmware repository' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n icon=\"c8y-firmware\"\n label=\"{{ (firmware$ | async)?.name }}\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Add firmware' | translate }}\"\n type=\"button\"\n *ngIf=\"!(isLegacy$ | async)\"\n data-cy=\"firmware-details--add-firmware-btn\"\n (click)=\"addBaseVersion()\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add firmware' | translate }}\n </