@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 141 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 { AsyncPipe, NgClass } from '@angular/common';\nimport { Component, EventEmitter, Output, ViewChild } from '@angular/core';\nimport { FormsModule, NgForm } from '@angular/forms';\nimport { IManagedObject } from '@c8y/client';\nimport {\n AlertService,\n C8yTranslateDirective,\n C8yTranslatePipe,\n FilePickerComponent,\n FormGroupComponent,\n ForOfDirective,\n HighlightComponent,\n IconDirective,\n ListItemComponent,\n PickedFiles,\n ProductExperienceDirective,\n RequiredInputPlaceholderDirective,\n TypeaheadComponent,\n ValidationPattern\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n ModalModel,\n PRODUCT_EXPERIENCE_REPOSITORY_SHARED,\n RepositoryCategory,\n RepositoryService,\n RepositoryType,\n SoftwareTypeComponent\n} from '@c8y/ngx-components/repository/shared';\nimport { assign, get, isUndefined } from 'lodash-es';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { PopoverDirective } from 'ngx-bootstrap/popover';\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 imports: [\n IconDirective,\n C8yTranslateDirective,\n FormsModule,\n FormGroupComponent,\n TypeaheadComponent,\n ForOfDirective,\n ListItemComponent,\n HighlightComponent,\n RequiredInputPlaceholderDirective,\n PopoverDirective,\n SoftwareTypeComponent,\n FilePickerComponent,\n ProductExperienceDirective,\n NgClass,\n AsyncPipe,\n C8yTranslatePipe\n ]\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\n class=\"modal-title\"\n id=\"addSoftwareModalTitle\"\n translate\n >\n Add software\n </div>\n </div>\n @if (!softwarePreselected) {\n <div class=\"p-16 text-center separator-bottom\">\n <p\n class=\"text-medium text-16\"\n translate\n >\n Select or create new software\n </p>\n </div>\n }\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\n class=\"modal-body\"\n id=\"addSoftwareModalDescription\"\n >\n <div [hidden]=\"softwarePreselected\">\n <c8y-form-group>\n <label\n for=\"softwareName\"\n translate\n >\n Software\n </label>\n <c8y-typeahead\n placeholder=\"{{ 'Select or enter' | translate }}\"\n name=\"softwareName\"\n [(ngModel)]=\"model.selected\"\n (onSearch)=\"onInput.next($event)\"\n [required]=\"true\"\n >\n <c8y-li\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n *c8yFor=\"\n let software of softwaresResult;\n loadMore: 'auto';\n notFound: notFoundTemplate\n \"\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 @if ((onInput | async)?.length > 0) {\n <c8y-li class=\"bg-level-2 p-8\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n title=\"{{ 'Add new`software`' | translate }}\"\n type=\"button\"\n >\n {{ 'Add new`software`' | translate }}\n </button>\n </c8y-li>\n }\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n\n <c8y-form-group>\n <label\n for=\"softwareDescription\"\n translate\n >\n Description\n </label>\n <input\n class=\"form-control\"\n id=\"softwareDescription\"\n placeholder=\"{{ 'e.g. Cloud connectivity software' | translate }}\"\n name=\"description\"\n autocomplete=\"off\"\n [(ngModel)]=\"model.description\"\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\n class=\"control-label\"\n for=\"softwareDeviceTypeFilter\"\n >\n {{ 'Device type filter' | translate }}\n <button\n class=\"btn-help\"\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 type=\"button\"\n ></button>\n </label>\n <input\n class=\"form-control\"\n id=\"softwareDeviceTypeFilter\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n name=\"softwareDeviceTypeFilter\"\n [(ngModel)]=\"model.deviceType\"\n [disabled]=\"model.selected?.id\"\n [pattern]=\"ValidationPattern.rules.noWhiteSpaceOnly.pattern\"\n />\n </c8y-form-group>\n\n <c8y-form-group>\n <label\n for=\"softwareType\"\n translate\n >\n Software type\n </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\n for=\"softwareVersion\"\n translate\n >\n Version\n </label>\n <input\n class=\"form-control\"\n id=\"softwareVersion\"\n placeholder=\"{{ 'e.g.' | translate }} 1.0.0\"\n name=\"version\"\n autocomplete=\"off\"\n [(ngModel)]=\"model.version\"\n [required]=\"true\"\n [pattern]=\"ValidationPattern.rules.noWhiteSpaceOnly.pattern\"\n />\n </c8y-form-group>\n\n <c8y-form-group>\n <div\n class=\"legend form-block m-t-40\"\n translate\n >\n Software file\n </div>\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'provided']\"\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 title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving\"\n >\n {{ 'Cancel' | translate }}\n </button>\n\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Add software' | translate }}\"\n type=\"submit\"\n [ngClass]=\"{ 'btn-pending': saving }\"\n [disabled]=\"\n !softwareForm.form.valid ||\n softwareForm.form.pristine ||\n saving ||\n (model.binary?.url == null && !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 { gettext } from '@c8y/ngx-components/gettext';\nimport {\n ActionControl,\n AlertService,\n BuiltInActionType,\n Column,\n DataGridService,\n DataSourceModifier,\n ModalService,\n ServerSideDataCallback,\n ServerSideDataResult,\n Status,\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n IconDirective,\n HelpComponent,\n DataGridComponent,\n EmptyStateContextDirective,\n EmptyStateComponent,\n C8yTranslatePipe\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 imports: [\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n IconDirective,\n HelpComponent,\n DataGridComponent,\n EmptyStateContextDirective,\n EmptyStateComponent,\n C8yTranslatePipe\n ]\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 @if (stats?.size === 0) {\n <p>\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 }\n </c8y-ui-empty-state>\n </c8y-data-grid>\n</div>\n","import { AsyncPipe, NgClass, NgStyle } from '@angular/common';\nimport { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { IManagedObject, InventoryService, IResultList } from '@c8y/client';\nimport {\n ActionBarItemComponent,\n AlertService,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n C8yTranslateDirective,\n C8yTranslatePipe,\n EmptyStateComponent,\n FormGroupComponent,\n ForOfDirective,\n GainsightService,\n IconDirective,\n ListGroupComponent,\n ListItemBodyComponent,\n ListItemComponent,\n ListItemIconComponent,\n memoize,\n ModalService,\n RequiredInputPlaceholderDirective,\n Status,\n TitleComponent\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n FileDownloadComponent,\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 { PopoverDirective } from 'ngx-bootstrap/popover';\nimport { TooltipDirective } from 'ngx-bootstrap/tooltip';\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 imports: [\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n IconDirective,\n NgClass,\n FormGroupComponent,\n RequiredInputPlaceholderDirective,\n FormsModule,\n NgStyle,\n PopoverDirective,\n SoftwareTypeComponent,\n C8yTranslateDirective,\n EmptyStateComponent,\n ListGroupComponent,\n ForOfDirective,\n ListItemComponent,\n ListItemIconComponent,\n ListItemBodyComponent,\n FileDownloadComponent,\n TooltipDirective,\n AsyncPipe,\n C8yTranslatePipe\n ]\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 icon=\"c8y-management\"\n label=\"{{ 'Management' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n icon=\"c8y-tools\"\n path=\"#/software\"\n label=\"{{ 'Software repository' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n icon=\"c8y-tools\"\n label=\"{{ (software$ | async)?.name }}\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n @if (!(isLegacy$ | async)) {\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Add software' | translate }}\"\n type=\"button\"\n (click)=\"addBaseVersion()\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Add software' | translate }}\n </button>\n }\n</c8y-action-bar-item>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n type=\"button\"\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 class=\"form-control\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.name?.length + 2 || 31 }\"\n placeholder=\"{{ 'e.g. My software' | translate }}\"\n type=\"text\"\n required\n #nameInput\n [ngModel]=\"(software$ | async)?.name\"\n #nameModel=\"ngModel\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\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 class=\"form-control\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.description?.length + 2 || 31 }\"\n placeholder=\"{{ 'e.g. Cloud connectivity software' | translate }}\"\n type=\"text\"\n #descriptionInput\n [ngModel]=\"(software$ | async)?.description\"\n #descriptionModel=\"ngModel\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\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 [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 type=\"button\"\n >\n <i c8yIcon=\"question-circle-o\"></i>\n </button>\n </label>\n <div class=\"input-group input-group-editable\">\n <input\n class=\"form-control\"\n [ngStyle]=\"{ 'width.ch': (software$ | async)?.type?.length + 2 || 31 }\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n type=\"text\"\n #deviceTypeInput\n [ngModel]=\"(software$ | async)?.c8y_Filter?.type\"\n #deviceTypeModel=\"ngModel\"\n />\n <span></span>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\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 [style]=\"{ 'width.ch': softwareTypeObject?.softwareType?.length + 2 || 31 }\"\n [softwareTypeMO]=\"softwareTypeObject\"\n (onSelectSoftware)=\"onSelectSoftwareType($event)\"\n ></c8y-software-type>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\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 @if ((baseVersions$ | async)?.data.length === 0) {\n <c8y-ui-empty-state\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 title=\"{{ 'Add software' | translate }}\"\n type=\"button\"\n (click)=\"addBaseVersion()\"\n >\n {{ 'Add software' | translate }}\n </button>\n </p>\n </c8y-ui-empty-state>\n }\n\n @if ((baseVersions$ | async)?.data.length > 0) {\n <c8y-list-group>\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 <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 @if (baseVersion?.c8y_Software?.url === '') {\n <span title=\"{{ 'Provided by device' | translate }}\">\n {{ 'Provided by device' | translate }}\n </span>\n } @else {\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 }\n </p>\n </div>\n <div class=\"col-2 d-flex a-i-start\">\n @if (isLegacy$ | async) {\n <span class=\"label label-warning m-l-auto-sm\">\n {{ 'Legacy' | translate }}\n </span>\n }\n @if (!(isLegacy$ | async)) {\n <div class=\"fit-h-20\">\n <button\n class=\"btn btn-danger btn-xs visible-xs m-l-auto m-r-8 m-t-8\"\n title=\"{{ 'Delete' | translate }}\"\n type=\"button\"\n (click)=\"deleteBaseVersion(baseVersion)\"\n >\n <i c8yIcon=\"delete\"></i>\n {{ 'Delete' | translate }}\n </button>\n </div>\n }\n </div>\n @if (!(isLegacy$ | async)) {\n <div class=\"m-l-auto fit-h-20 hidden-xs\">\n <button\n class=\"btn btn-dot text-danger showOnHover\"\n [attr.aria-label]=\"'Delete' | translate\"\n tooltip=\"{{ 'Delete' | translate }}\"\n type=\"button\"\n data-cy=\"software-details-component--Delete-button\"\n [delay]=\"500\"\n (click)=\"deleteBaseVersion(baseVersion)\"\n >\n <i c8yIcon=\"delete\"></i>\n </button>\n </div>\n }\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Injectable } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { 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 { ModuleWithProviders, NgModule } from '@angular/core';\nimport { hookNavigator, hookRoute } from '@c8y/ngx-components';\nimport { SharedRepositoryModule } from '@c8y/ngx-components/repository/shared';\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 SharedRepositoryModule,\n SoftwareListComponent,\n SoftwareDetailsComponent,\n AddSoftwareModalComponent\n ]\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/gettext';\nimport {\n ListGroupComponent,\n ForOfDirective,\n ListItemComponent,\n ListItemIconComponent,\n IconDirective,\n ListItemBodyComponent,\n C8yTranslateDirective,\n ProductExperienceDirective,\n C8yTranslatePipe\n} 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';\nimport { NgClass, NgIf } from '@angular/common';\nimport { TooltipDirective } from 'ngx-bootstrap/tooltip';\n\n@Component({\n selector: 'c8y-device-software-list',\n templateUrl: 'device-software-list.component.html',\n imports: [\n ListGroupComponent,\n ForOfDirective,\n ListItemComponent,\n NgClass,\n ListItemIconComponent,\n IconDirective,\n ListItemBodyComponent,\n C8yTranslate