@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 140 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-repository-software.mjs","sources":["../../repository/software/list/add-software-modal.component.ts","../../repository/software/list/add-software-modal.component.html","../../repository/software/list/software-list.component.ts","../../repository/software/list/software-list.component.html","../../repository/software/list/software-details.component.ts","../../repository/software/list/software-details.component.html","../../repository/software/list/software-repository-navigation-factory.ts","../../repository/software/list/software-repository-list.module.ts","../../repository/software/device-tab/device-software.service.ts","../../repository/software/device-tab/device-software-list.component.ts","../../repository/software/device-tab/device-software-list.component.html","../../repository/software/device-tab/device-software-changes.component.ts","../../repository/software/device-tab/device-software-changes.component.html","../../repository/software/device-tab/installed-software.component.ts","../../repository/software/device-tab/installed-software.component.html","../../repository/software/device-tab/software-device-tab.component.ts","../../repository/software/device-tab/software-device-tab.component.html","../../repository/software/device-tab/software-device-tab.guard.ts","../../repository/software/device-tab/software-repository-device-tab.module.ts","../../repository/software/software-repository.module.ts","../../repository/software/c8y-ngx-components-repository-software.ts"],"sourcesContent":["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-software-software-modal',\n templateUrl: 'add-software-modal.component.html'\n})\nexport class AddSoftwareModalComponent {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED;\n @ViewChild('softwareForm', { 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 softwareType: undefined,\n binary: {\n file: undefined,\n url: undefined\n }\n };\n\n softwaresResult;\n saving = false;\n softwarePreselected = false;\n textForSoftwareUrlPopover: string =\n gettext(`Path for binaries can vary depending on device agent implementation, for example:\n /software/binaries/software1.bin\n https://software/binary/123\n ftp://software/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.loadSoftwares();\n }\n\n setInitialState() {\n if (this.model.selected) {\n this.softwarePreselected = true;\n }\n }\n\n loadSoftwares() {\n this.inputSubscription$ = this.onInput\n .pipe(\n tap(() => {\n if (!this.softwarePreselected) {\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.getSoftwareResult(searchStr))\n )\n .subscribe(result => {\n this.softwaresResult = result;\n });\n }\n\n getSoftwareResult(searchStr: string) {\n return from(\n this.repositoryService.listRepositoryEntries(RepositoryType.SOFTWARE, {\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.SOFTWARE)\n .then(savedSoftware => {\n this.successMsg();\n this.saving = false;\n this.saved.next(savedSoftware);\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('Software 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 onSoftwareSelect(software: IManagedObject) {\n assign(this.model, {\n selected: software,\n description: software.description,\n deviceType: get(software, 'c8y_Filter.type'),\n softwareType: software\n });\n }\n}\n","<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'c8y-tools'\"></i>\n <div class=\"modal-title\" translate id=\"addSoftwareModalTitle\">Add software</div>\n </div>\n <div class=\"p-16 text-center separator-bottom\" *ngIf=\"!softwarePreselected\">\n <p class=\"text-medium text-16\" translate>Select or create new software</p>\n </div>\n <form\n class=\"d-contents\"\n autocomplete=\"off\"\n #softwareForm=\"ngForm\"\n (ngSubmit)=\"softwareForm.form.valid && save()\"\n >\n <div class=\"modal-inner-scroll\">\n <div class=\"modal-body\" id=\"addSoftwareModalDescription\">\n <div [hidden]=\"softwarePreselected\">\n <c8y-form-group>\n <label for=\"softwareName\" translate>Software</label>\n <c8y-typeahead\n [(ngModel)]=\"model.selected\"\n name=\"softwareName\"\n placeholder=\"{{ 'Select or enter' | translate }}\"\n (onSearch)=\"onInput.next($event)\"\n [required]=\"true\"\n >\n <c8y-li\n *c8yFor=\"\n let software of softwaresResult;\n loadMore: 'auto';\n notFound: notFoundTemplate\n \"\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n (click)=\"onSoftwareSelect(software)\"\n [active]=\"model.selected === software\"\n >\n <c8y-highlight\n [text]=\"software.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`software`' | translate }}\"\n >\n {{ 'Add new`software`' | translate }}\n </button>\n </c8y-li>\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n\n <c8y-form-group>\n <label for=\"softwareDescription\" translate>Description</label>\n <input\n id=\"softwareDescription\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"model.description\"\n placeholder=\"{{ 'e.g. Cloud connectivity software' | 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=\"softwareDeviceTypeFilter\">\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 software 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 placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n <input\n id=\"softwareDeviceTypeFilter\"\n class=\"form-control\"\n name=\"softwareDeviceTypeFilter\"\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\n <c8y-form-group>\n <label for=\"softwareType\" translate>Software type</label>\n <c8y-software-type\n name=\"softwareType\"\n [(ngModel)]=\"model.softwareType\"\n [disabled]=\"model.selected?.id\"\n ></c8y-software-type>\n </c8y-form-group>\n </div>\n\n <c8y-form-group>\n <label for=\"softwareVersion\" translate>Version</label>\n <input\n id=\"softwareVersion\"\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>Software file</div>\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n (onFilesPicked)=\"onFile($event)\"\n [fileUrlPopover]=\"textForSoftwareUrlPopover\"\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 (click)=\"cancel()\"\n [disabled]=\"saving\"\n >\n {{ 'Cancel' | translate }}\n </button>\n\n <button\n class=\"btn btn-primary\"\n type=\"submit\"\n title=\"{{ 'Add software' | translate }}\"\n [ngClass]=\"{ 'btn-pending': saving }\"\n [disabled]=\"\n !softwareForm.form.valid ||\n softwareForm.form.pristine ||\n saving ||\n (!model.binary?.url && !model.binary?.file)\n \"\n c8yProductExperience\n [actionName]=\"PRODUCT_EXPERIENCE.SOFTWARE.EVENTS.REPOSITORY\"\n [actionData]=\"{\n component: PRODUCT_EXPERIENCE.SOFTWARE.COMPONENTS.ADD_SOFTWARE_MODAL,\n result:\n softwarePreselected || model.selected?.id\n ? PRODUCT_EXPERIENCE.SOFTWARE.RESULTS.ADD_SOFTWARE_VERSION\n : PRODUCT_EXPERIENCE.SOFTWARE.RESULTS.ADD_SOFTWARE\n }\"\n >\n {{ 'Add software' | translate }}\n </button>\n </div>\n </form>\n</div>\n","import { Component, EventEmitter, OnInit } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { IManagedObject, IResultList } from '@c8y/client';\nimport {\n ActionControl,\n AlertService,\n BuiltInActionType,\n Column,\n DataGridService,\n DataSourceModifier,\n ModalService,\n ServerSideDataCallback,\n ServerSideDataResult,\n Status,\n gettext\n} from '@c8y/ngx-components';\nimport {\n DescriptionGridColumn,\n DeviceTypeGridColumn,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryItemNameGridColumn,\n RepositoryService,\n RepositoryType,\n TypeGridColumn,\n VersionsGridColumn\n} from '@c8y/ngx-components/repository/shared';\nimport { TranslateService } from '@ngx-translate/core';\nimport { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';\nimport { AddSoftwareModalComponent } from './add-software-modal.component';\n\n@Component({\n selector: 'c8y-software-list',\n templateUrl: 'software-list.component.html'\n})\nexport class SoftwareListComponent implements OnInit {\n sizeRequest: Promise<number>;\n sizeRequestDone = false;\n refresh$: EventEmitter<void> = new EventEmitter();\n\n columns: Column[] = [\n new RepositoryItemNameGridColumn({\n filterLabel: gettext('Filter software by name')\n }),\n new DescriptionGridColumn({\n filterLabel: gettext('Filter software by description'),\n placeholder: gettext('Cloud connectivity software')\n }),\n new DeviceTypeGridColumn({ filterLabel: gettext('Filter software by device type') }),\n new TypeGridColumn({\n header: gettext('Software type'),\n filterLabel: gettext('Filter by software type'),\n example: 'yum',\n path: 'softwareType',\n repositoryType: RepositoryType.SOFTWARE\n }),\n new VersionsGridColumn()\n ];\n actionControls: ActionControl[] = [];\n serverSideDataCallback: ServerSideDataCallback;\n pagination = {\n pageSize: 50,\n currentPage: 1\n };\n\n noResultsMessage = gettext('No results to display.');\n noDataMessage = gettext('No software to display.');\n noResultsSubtitle = gettext('Refine your search terms or check your spelling.');\n noDataSubtitle = gettext('Add a new software by clicking below.');\n\n constructor(\n private repositoryService: RepositoryService,\n private gridService: DataGridService,\n private modalService: ModalService,\n private bsModalService: BsModalService,\n private translateService: TranslateService,\n private alertService: AlertService,\n private router: Router,\n private activatedRoute: ActivatedRoute\n ) {\n this.serverSideDataCallback = this.onDataSourceModifier.bind(this);\n }\n\n ngOnInit(): void {\n this.actionControls.push({\n type: BuiltInActionType.Edit,\n callback: this.editSoftware.bind(this)\n });\n this.actionControls.push({\n type: BuiltInActionType.Delete,\n callback: this.deleteSoftware.bind(this)\n });\n }\n\n async onDataSourceModifier(\n dataSourceModifier: DataSourceModifier\n ): Promise<ServerSideDataResult> {\n const dataRequest: Promise<IResultList<IManagedObject>> =\n this.repositoryService.listRepositoryEntries(RepositoryType.SOFTWARE, {\n query: this.gridService.getQueryObj(dataSourceModifier.columns),\n skipDefaultOrder: true,\n params: {\n pageSize: dataSourceModifier.pagination.pageSize,\n currentPage: dataSourceModifier.pagination.currentPage\n }\n });\n\n const filtererdSizeRequest: Promise<number> = this.repositoryService\n .listRepositoryEntries(RepositoryType.SOFTWARE, {\n skipDefaultOrder: true,\n query: this.gridService.getQueryObj(dataSourceModifier.columns),\n params: { pageSize: 1 }\n })\n .then(response => response?.paging?.totalPages);\n\n this.sizeRequest = this.repositoryService\n .listRepositoryEntries(RepositoryType.SOFTWARE, {\n skipDefaultOrder: true,\n params: { pageSize: 1 }\n })\n .then(response => {\n this.sizeRequestDone = true;\n return response?.paging?.totalPages;\n });\n\n const [dataResponse, size, filteredSize] = await Promise.all([\n dataRequest,\n this.sizeRequest,\n filtererdSizeRequest\n ]);\n\n const { res, data, paging } = dataResponse;\n\n const serverSideDataResult: ServerSideDataResult = {\n res,\n data,\n paging,\n filteredSize,\n size\n };\n\n return serverSideDataResult;\n }\n\n addSoftware() {\n const config: ModalOptions<AddSoftwareModalComponent> = {\n class: 'modal-sm',\n ariaDescribedby: 'addSoftwareModalDescription',\n ariaLabelledBy: 'addSoftwareModalTitle',\n ignoreBackdropClick: true,\n keyboard: false\n };\n const modalRef = this.bsModalService.show(AddSoftwareModalComponent, config);\n modalRef.content.saved.subscribe(savedSoftware => this.editSoftware(savedSoftware));\n }\n\n editSoftware(software: Partial<IManagedObject>) {\n this.router.navigate([software.id], { relativeTo: this.activatedRoute });\n }\n\n async deleteSoftware(software: IManagedObject) {\n try {\n const title = gettext('Delete software');\n const body = `\n ${this.translateService.instant(\n gettext('You are about to delete software \"{{ name }}\" with all its versions.'),\n { name: software.name }\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.SOFTWARE.EVENTS.REPOSITORY }\n );\n await this.repositoryService.delete(software);\n this.alertService.success(gettext('Software deleted.'));\n this.refresh$.next();\n } catch (ex) {\n // only if not cancel from modal\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n trackByName(_index, column: Column): string {\n return column.name;\n }\n}\n","<c8y-title>\n {{ 'Software repository' | translate }}\n</c8y-title>\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-tools\"\n label=\"{{ 'Software repository' | translate }}\"\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 software' | translate }}\"\n type=\"button\"\n (click)=\"addSoftware()\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add software' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<c8y-help\n src=\"/docs/device-management-application/managing-device-data/#managing-software\"\n></c8y-help>\n\n<div class=\"content-fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"'Software' | translate\"\n [refresh]=\"refresh$\"\n [pagination]=\"pagination\"\n [columns]=\"columns\"\n [actionControls]=\"actionControls\"\n [infiniteScroll]=\"'auto'\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n >\n <c8y-ui-empty-state\n [icon]=\"stats?.size > 0 ? 'search' : 'c8y-tools'\"\n [title]=\"stats?.size > 0 ? (noResultsMessage | translate) : (noDataMessage | translate)\"\n [subtitle]=\"stats?.size > 0 ? (noResultsSubtitle | translate) : (noDataSubtitle | translate)\"\n *emptyStateContext=\"let stats\"\n [horizontal]=\"stats?.size > 0\"\n >\n <p *ngIf=\"stats?.size === 0\">\n <button\n class=\"btn btn-primary\"\n [title]=\"'Add software' | translate\"\n type=\"button\"\n (click)=\"addSoftware()\"\n >\n {{ 'Add software' | translate }}\n </button>\n </p>\n </c8y-ui-empty-state>\n <ng-container *ngFor=\"let column of columns; trackBy: trackByName\">\n <c8y-column [name]=\"column.name\"></c8y-column>\n </ng-container>\n </c8y-data-grid>\n</div>\n","import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { IManagedObject, InventoryService, IResultList } from '@c8y/client';\nimport {\n AlertService,\n GainsightService,\n gettext,\n memoize,\n ModalService,\n Status\n} from '@c8y/ngx-components';\nimport {\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryService,\n SoftwareTypeComponent\n} from '@c8y/ngx-components/repository/shared';\nimport { TranslateService } from '@ngx-translate/core';\nimport { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, from, merge, Observable, Subject } from 'rxjs';\nimport {\n distinctUntilKeyChanged,\n map,\n shareReplay,\n switchMap,\n take,\n takeUntil,\n tap,\n withLatestFrom\n} from 'rxjs/operators';\nimport { AddSoftwareModalComponent } from './add-software-modal.component';\n\n@Component({\n selector: 'c8y-software-details',\n templateUrl: './software-details.component.html'\n})\nexport class SoftwareDetailsComponent implements OnInit, OnDestroy {\n @ViewChild(SoftwareTypeComponent, { static: true })\n softwareType: SoftwareTypeComponent;\n\n reload$: Subject<void> = new Subject();\n reloading$: BehaviorSubject<boolean> = new BehaviorSubject(false);\n softwareTypeObject: IManagedObject;\n isSoftwareTypeChanged = false;\n\n updateSoftware$: Subject<Partial<IManagedObject>> = new Subject();\n softwareUpdated$: Subject<IManagedObject> = new Subject();\n baseVersionsUpdated$: Subject<void> = new Subject();\n\n software$: Observable<IManagedObject> = merge(\n this.activatedRoute.params.pipe(\n map(params => params.id),\n switchMap(id => from(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 => from(this.inventoryService.detail(id).then(result => result.data))),\n tap(() => this.reloading$.next(false))\n ),\n this.softwareUpdated$\n ).pipe(shareReplay(1));\n\n baseVersions$: Observable<IResultList<IManagedObject>> = merge(\n this.software$.pipe(distinctUntilKeyChanged('id')),\n this.baseVersionsUpdated$,\n this.reload$\n ).pipe(\n switchMap(() => this.software$),\n switchMap(software => this.repositoryService.listBaseVersions(software)),\n shareReplay(1)\n );\n\n isLegacy$: Observable<boolean> = this.software$.pipe(\n map(software => this.repositoryService.isLegacyEntry(software)),\n shareReplay(1)\n );\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.updateSoftware$\n .pipe(\n withLatestFrom(this.software$),\n switchMap(([softwarePartial, software]) =>\n this.inventoryService.update({\n id: software.id,\n ...softwarePartial\n })\n ),\n map(({ data }) => data),\n tap(software => this.softwareUpdated$.next(software)),\n tap(() => this.alertService.success(gettext('Saved.'))),\n tap(() =>\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED.SOFTWARE.EVENTS.REPOSITORY,\n {\n result: PRODUCT_EXPERIENCE_REPOSITORY_SHARED.SOFTWARE.RESULTS.EDIT_SOFTWARE\n }\n )\n ),\n takeUntil(this.destroy$)\n )\n .subscribe();\n\n this.software$.subscribe(software => {\n this.softwareTypeObject = software;\n });\n }\n\n @memoize()\n getBinaryName$(binaryUrl) {\n return this.repositoryService.getBinaryName$(binaryUrl);\n }\n\n addBaseVersion() {\n this.software$\n .pipe(\n take(1),\n switchMap(software => {\n const initialState = {\n model: {\n selected: software,\n description: software.description\n }\n };\n const config: ModalOptions<AddSoftwareModalComponent> = {\n class: 'modal-sm',\n ariaDescribedby: 'addSoftwareModalDescription',\n ariaLabelledBy: 'addSoftwareModalTitle',\n ignoreBackdropClick: true,\n keyboard: false,\n initialState\n };\n const modalRef = this.bsModalService.show(AddSoftwareModalComponent, config);\n return modalRef.content.saved;\n })\n )\n .subscribe(() => this.baseVersionsUpdated$.next());\n }\n\n async deleteBaseVersion(baseVersion: IManagedObject) {\n try {\n const title = gettext('Delete software');\n const body = `\n ${this.translateService.instant(\n gettext('You are about to delete software {{ version }}.'),\n { version: baseVersion.c8y_Software.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(title, body, Status.DANGER, labels);\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.softwareTypeObject);\n this.router.navigateByUrl('/software');\n } else {\n await this.repositoryService.delete(baseVersion);\n this.baseVersionsUpdated$.next();\n }\n this.alertService.success(gettext('Software deleted.'));\n } catch (ex) {\n // only if not cancel from modal\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n onSelectSoftwareType(software) {\n this.isSoftwareTypeChanged = !(\n this.softwareTypeObject?.softwareType === software?.softwareType\n );\n this.softwareTypeObject = software;\n }\n\n ngOnDestroy() {\n this.destroy$.next(true);\n this.destroy$.unsubscribe();\n }\n}\n","<c8y-title>\n {{ (software$ | async)?.name }}\n</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n label=\"{{ 'Management' | translate }}\"\n icon=\"c8y-management\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n path=\"#/software\"\n label=\"{{ 'Software repository' | translate }}\"\n icon=\"c8y-tools\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n label=\"{{ (software$ | async)?.name }}\"\n icon=\"c8y-tools\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n type=\"button\"\n title=\"{{ 'Add software' | translate }}\"\n (click)=\"addBaseVersion()\"\n *ngIf=\"!(isLegacy$ | async)\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add software' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n type=\"button\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"reload$.next()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': reloading$ | async }\"\n ></i>\n {{ 'Reload' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<div class=\"row\">\n <div class=\"col-lg-12 col-lg-max\">\n <div class=\"card card--fullpage\">\n <div class=\"card-block bg-level-1 flex-no-shrink p-t-24 p-b-24 overflow-visible\">\n <div class=\"content-flex-70\">\n <div class=\"text-center\">\n <i class=\"c8y-icon-duocolor icon-48 c8y-icon c8y-icon-tools\"></i>\n <p>\n <small class=\"label label-info\">Software</small>\n </p>\n </div>\n <div class=\"flex-grow col-10\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"control-label\">\n {{ 'Name' | translate }}\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n #nameInput\n type=\"text\"\n class=\"form-control\"\n [ngModel]=\"(software$ | async)?.name\"\n #nameModel=\"ngModel\"\n placeholder=\"{{ 'e.g. My software' | translate }}\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.name?.length + 2 || 31 }\"\n required\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n type=\"button\"\n title=\"{{ 'Save' | translate }}\"\n (click)=\"updateSoftware$.next({ name: nameInput.value }); nameModel.reset()\"\n [disabled]=\"nameInput.value.length === 0\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"control-label\">\n {{ 'Description' | translate }}\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n #descriptionInput\n type=\"text\"\n class=\"form-control\"\n [ngModel]=\"(software$ | async)?.description\"\n #descriptionModel=\"ngModel\"\n placeholder=\"{{ 'e.g. Cloud connectivity software' | translate }}\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.description?.length + 2 || 31 }\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n type=\"button\"\n title=\"{{ 'Save' | translate }}\"\n (click)=\"\n updateSoftware$.next({ description: descriptionInput.value });\n descriptionModel.reset()\n \"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </div>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"control-label\">\n {{ 'Device type' | 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 software 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 container=\"body\"\n >\n <i c8yIcon=\"question-circle-o\"></i>\n </button>\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n #deviceTypeInput\n type=\"text\"\n class=\"form-control\"\n [ngModel]=\"(software$ | async)?.c8y_Filter?.type\"\n #deviceTypeModel=\"ngModel\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.type?.length + 2 || 31 }\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n type=\"button\"\n title=\"{{ 'Save' | translate }}\"\n (click)=\"\n updateSoftware$.next({ c8y_Filter: { type: deviceTypeInput.value } });\n deviceTypeModel.reset()\n \"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"control-label\">\n {{ 'Software type' | translate }}\n </label>\n <div class=\"input-group input-group-editable\">\n <c8y-software-type\n [softwareTypeMO]=\"softwareTypeObject\"\n [style]=\"{ 'width.ch': softwareTypeObject?.softwareType?.length + 2 || 31 }\"\n (onSelectSoftware)=\"onSelectSoftwareType($event)\"\n ></c8y-software-type>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n type=\"button\"\n title=\"{{ 'Save' | translate }}\"\n [ngClass]=\"isSoftwareTypeChanged ? '' : 'hidden'\"\n [disabled]=\"softwareTypeObject?.softwareType === ''\"\n (click)=\"\n updateSoftware$.next({ softwareType: softwareTypeObject.softwareType });\n softwareType.resetInput();\n isSoftwareTypeChanged = false\n \"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n </div>\n </c8y-form-group>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"inner-scroll\">\n <div class=\"card-header separator-top-bottom sticky-top bg-component\">\n <div\n class=\"card-title\"\n translate\n >\n Versions\n </div>\n </div>\n\n <div class=\"card-block p-t-0 p-b-24\">\n <c8y-ui-empty-state\n *ngIf=\"(baseVersions$ | async)?.data.length === 0\"\n [icon]=\"'c8y-tools'\"\n [title]=\"'No versions to display.' | translate\"\n [subtitle]=\"'Add a new version by clicking below.' | translate\"\n >\n <p>\n <button\n class=\"btn btn-primary\"\n type=\"button\"\n title=\"{{ 'Add software' | translate }}\"\n (click)=\"addBaseVersion()\"\n >\n {{ 'Add software' | translate }}\n </button>\n </p>\n </c8y-ui-empty-state>\n\n <c8y-list-group *ngIf=\"(baseVersions$ | async)?.data.length > 0\">\n <c8y-li\n *c8yFor=\"let baseVersion of baseVersions$ | async; let i = index; loadMore: 'auto'\"\n >\n <c8y-li-icon>\n <i c8yIcon=\"c8y-tools\"></i>\n </c8y-li-icon>\n\n <c8y-li-body class=\"content-flex-50\">\n <div class=\"col-4\">\n <p\n class=\"text-truncate-wrap\"\n title=\"{{ baseVersion.c8y_Software.version }}\"\n >\n {{ baseVersion.c8y_Software.version }}\n </p>\n </div>\n <div class=\"col-5\">\n <p class=\"text-truncate-wrap\">\n <span\n class=\"text-label-small m-r-8\"\n translate\n >\n File\n </span>\n <span title=\" {{ getBinaryName$(baseVersion.c8y_Software.url) | async }}\">\n <c8y-file-download\n url=\"{{ baseVersion.c8y_Software.url }}\"\n ></c8y-file-download>\n </span>\n </p>\n </div>\n <div class=\"col-2 d-flex a-i-start\">\n <span\n *ngIf=\"isLegacy$ | async\"\n class=\"label label-warning m-l-auto-sm\"\n >\n {{ 'Legacy' | translate }}\n </span>\n\n <div\n class=\"fit-h-20\"\n *ngIf=\"!(isLegacy$ | async)\"\n >\n <button\n class=\"btn btn-danger btn-xs visible-xs m-l-auto m-r-8 m-t-8\"\n type=\"button\"\n title=\"{{ 'Delete' | translate }}\"\n (click)=\"deleteBaseVersion(baseVersion)\"\n >\n <i c8yIcon=\"delete\"></i>\n {{ 'Delete' | translate }}\n </button>\n </div>\n </div>\n <div\n *ngIf=\"!(isLegacy$ | async)\"\n class=\"m-l-auto fit-h-20 hidden-xs\"\n >\n <button\n class=\"btn btn-dot text-danger showOnHover\"\n type=\"button\"\n data-cy=\"software-details-component--Delete-button\"\n [attr.aria-label]=\"'Delete' | translate\"\n tooltip=\"{{ 'Delete' | translate }}\"\n [delay]=\"500\"\n (click)=\"deleteBaseVersion(baseVersion)\"\n >\n <i c8yIcon=\"delete\"></i>\n </button>\n </div>\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Injectable } from '@angular/core';\nimport { gettext, NavigatorNode, NavigatorNodeFactory } from '@c8y/ngx-components';\n\n@Injectable()\nexport class SoftwareRepositoryNavigationFactory implements NavigatorNodeFactory {\n node: NavigatorNode;\n\n constructor() {\n this.node = new NavigatorNode({\n label: gettext('Software repository'),\n path: 'software',\n icon: 'c8y-tools',\n parent: gettext('Management'),\n priority: 900\n });\n }\n\n get(): NavigatorNode {\n return this.node;\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { ModuleWithProviders, NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { CoreModule, FormsModule, hookNavigator, hookRoute } from '@c8y/ngx-components';\nimport { DeviceGridModule } from '@c8y/ngx-components/device-grid';\nimport { SharedRepositoryModule } from '@c8y/ngx-components/repository/shared';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { AddSoftwareModalComponent } from './add-software-modal.component';\nimport { SoftwareDetailsComponent } from './software-details.component';\nimport { SoftwareListComponent } from './software-list.component';\nimport { SoftwareRepositoryNavigationFactory } from './software-repository-navigation-factory';\n\n@NgModule({\n imports: [\n CommonModule,\n CoreModule,\n FormsModule,\n DeviceGridModule,\n PopoverModule,\n TooltipModule,\n RouterModule,\n SharedRepositoryModule\n ],\n declarations: [SoftwareListComponent, SoftwareDetailsComponent, AddSoftwareModalComponent]\n})\nexport class SoftwareRepositoryListModule {\n static forRoot(): ModuleWithProviders<SoftwareRepositoryListModule> {\n return {\n ngModule: SoftwareRepositoryListModule,\n providers: [\n hookNavigator(SoftwareRepositoryNavigationFactory),\n hookRoute([\n {\n path: 'software',\n component: SoftwareListComponent\n },\n {\n path: 'software/:id',\n component: SoftwareDetailsComponent\n }\n ])\n ]\n };\n }\n}\n","import { Injectable } from '@angular/core';\nimport { IResultList, Paging } from '@c8y/client';\nimport { ServiceRegistry } from '@c8y/ngx-components';\nimport {\n DeviceSoftware,\n FilterCriteria,\n IAdvancedSoftwareService\n} from '@c8y/ngx-components/repository/shared';\nimport { head, set } from 'lodash-es';\nimport { BehaviorSubject, Observable, Subject } from 'rxjs';\nimport { share, switchMap } from 'rxjs/operators';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class DeviceSoftwareService {\n /**\n * Indicates whether device software data is currently being loaded.\n */\n get loading$(): Observable<boolean> {\n return this.loadingSubject$.asObservable().pipe(share());\n }\n\n private reloadSubject$ = new BehaviorSubject<void>(null);\n private loadingSubject$ = new Subject<boolean>();\n\n constructor(private serviceRegistry: ServiceRegistry) {}\n\n /**\n * Trigger device software data reload.\n */\n reload() {\n this.reloadSubject$.next();\n }\n\n /**\n * Retrieves the software list that is installed on a given device.\n *\n * @param deviceId ID of the device to retrieve software data for\n * @param filterCriteria Criteria that software items are filtered by.\n * @param legacySoftwareList If provided no data will be fetched from backend. The provided software list\n * will be filtered by the specified filter criteria.\n *\n * @returns The software items installed on the specified device filtered by the specified criteria.\n */\n getSoftwareList(\n deviceId,\n filterCriteria: FilterCriteria,\n legacySoftwareList?: DeviceSoftware[]\n ): Observable<IResultList<DeviceSoftware>> {\n this.loadingSubject$.next(true);\n\n return this.reloadSubject$.pipe(\n switchMap(() => {\n const softwareList$ = !!legacySoftwareList\n ? this.getLegacySoftwareList(legacySoftwareList, filterCriteria)\n : this.getAdvancedSoftwareList(deviceId, filterCriteria);\n return softwareList$.then(resultList => {\n this.loadingSubject$.next(false);\n return resultList;\n });\n }),\n share()\n );\n }\n\n private getAdvancedSoftwareList(\n deviceId,\n filterCriteria: FilterCriteria\n ): Promise<IResultList<DeviceSoftware>> {\n const queryFilter = {\n deviceId,\n currentPage: 1,\n pageSize: 50,\n withTotalPages: true\n };\n const advancedSoftwareService: IAdvancedSoftwareService = head(this.serviceRegistry.get('asm'));\n if (filterCriteria?.name) {\n set(queryFilter, 'name', `*${filterCriteria.name}*`);\n }\n if (filterCriteria?.softwareType) {\n set(queryFilter, 'type', `${filterCriteria.softwareType}`);\n }\n return advancedSoftwareService.list(queryFilter) as unknown as Promise<\n IResultList<DeviceSoftware>\n >;\n }\n\n private getLegacySoftwareList(\n legacySoftwareList: DeviceSoftware[],\n filterCriteria: FilterCriteria\n ): Promise<IResultList<DeviceSoftware>> {\n const data = filterCriteria\n ? legacySoftwareList.filter(item => {\n let match = true;\n if (filterCriteria?.name) {\n match = match && item.name?.includes(filterCriteria.name);\n }\n if (filterCriteria?.softwareType) {\n match = match && item.softwareType === filterCriteria.softwareType;\n }\n\n return match;\n })\n : legacySoftwareList;\n return Promise.resolve({\n data,\n res: null,\n paging: {\n totalPages: data.length\n } as Paging<DeviceSoftware>\n });\n }\n}\n","import {\n AfterContentInit,\n Component,\n EventEmitter,\n Input,\n OnDestroy,\n OnInit,\n Output\n} from '@angular/core';\nimport { IManagedObject, IResultList } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components';\nimport {\n DeviceSoftware,\n DeviceSoftwareChange,\n FilterCriteria,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED\n} from '@c8y/ngx-components/repository/shared';\nimport { filter, get } from 'lodash-es';\nimport { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';\nimport {\n debounceTime,\n distinctUntilChanged,\n map,\n share,\n switchMap,\n takeUntil,\n tap\n} from 'rxjs/operators';\nimport { DeviceSoftwareService } from './device-software.service';\n\n@Component({\n selector: 'c8y-device-software-list',\n templateUrl: 'device-software-list.component.html'\n})\nexport class DeviceSoftwareListComponent implements OnInit, AfterContentInit, OnDestroy {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_REPOSITORY_SHARED;\n @Input() set softwareList(softwareList: DeviceSoftware[]) {\n if (softwareList !== null) {\n this.legacySoftwareList$.next(softwareList);\n }\n }\n @Input() device: IManagedObject;\n @Input() deviceSoftwareChanges: DeviceSoftwareChange[];\n @Input() filterCriteria$: Observable<FilterCriteria> = of(null);\n @Output() update = new EventEmitter<DeviceSoftware>();\n @Output() remove = new EventEmitter<DeviceSoftware>();\n @Output() onListEmpty: EventEmitter<boolean> = new EventEmitter();\n softwareItems$: Observable<IResultList<DeviceSoftware>>;\n showUpdate: boolean;\n showRemove: boolean;\n emptyList: boolean;\n noSearchResults: boolean;\n alreadyInstalledMessage = gettext('This software is already installed on the device');\n supportsSoftwareOperations = false;\n\n private readonly operationTypes = ['c8y_SoftwareUpdate', 'c8y_SoftwareList', 'c8y_Software'];\n\n private legacySoftwareList$: BehaviorSubject<DeviceSoftware[]> = new BehaviorSubject([]);\n private destroyed$ = new Subject<void>();\n\n constructor(private deviceSoftwareService: DeviceSoftwareService) {}\n\n ngOnInit(): void {\n this.softwareItems$ = combineLatest([\n this.filterCriteria$.pipe(debounceTime(300), distinctUntilChanged()),\n this.legacySoftwareList$\n ]).pipe(\n switchMap(([filterCriteria, legacySoftwareList]) =>\n this.deviceSoftwareService\n .getSoftwareList(this.device?.id, filterCriteria, legacySoftwareList)\n .pipe(map(resultList => ({ resultList, filterCriteria })))\n ),\n tap(({ resultList, filterCriteria }) => {\n this.notifyListEmpty(!resultList?.paging?.totalPages && !filterCriteria);\n this.noSearchResults = !resultList?.paging?.totalPages && !!filterCriteria;\n }),\n map(({ resultList }) => resultList),\n share(),\n takeUntil(this.destroyed$)\n );\n const supportedOperations = get(this.device, 'c8y_SupportedOperations', []);\n this.supportsSoftwareOperations = this.operationTypes.some(\n operationType => supportedOperations.indexOf(operationType) > -1\n );\n }\n\n ngAfterContentInit() {\n this.showUpdate = this.update.observers.length > 0;\n this.showRemove = this.remove.observers.length > 0;\n }\n\n isSoftwareGoingToBeChanged(software: DeviceSoftware): boolean {\n const relevantChanges = filter(this.deviceSoftwareChanges, software);\n return relevantChanges.length > 0;\n }\n\n ngOnDestroy(): void {\n this.destroyed$.next();\n this.destroyed$.complete();\n }\n\n private notifyListEmpty(isEmpty: boolean): void {\n this.emptyList = isEmpty;\n this.onListEmpty.emit(isEmpty);\n }\n}\n","<c8y-list-group class=\"no-border-2nd-last\">\n <c8y-li\n [ngClass]=\"{ disabled: isSoftwareGoingToBeChanged(software) }\"\n *c8yFor=\"let software of softwareItems$\"\n >\n <!-- SOFTWARE ICON -->\n <c8y-li-icon>\n <i c8yIcon=\"c8y-tools\"></i>\n </c8y-li-icon>\n\n <c8y-li-body class=\"content-flex-20\">\n <div title=\"{{ software.name }}\" class=\"col-9\">\n <p class=\"d-flex\">\n <!-- SOFTWARE NAME -->\n <span class=\"text-truncate\">\n {{ software.name }}\n </span>\n <!-- SOFTWARE TYPE-->\n <span class=\"text-truncate\">\n <span class=\"label label-primary m-l-8\">{{ software.softwareType }}</span>\n </span>\n </p>\n <!-- SOFTWARE VERSION -->\n <p class=\"d-flex a-i-center\">\n <span class=\"text-truncate text-label-small m-r-4\" translate>Version</span>\n <span class=\"text-truncate m-r-4\" title=\"{{ software.version }}\">\n {{ software.version }}\n </span>\n