UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 126 kB
{"version":3,"file":"c8y-ngx-components-ecosystem-application-plugins.mjs","sources":["../../ecosystem/application-plugins/only-latest-filter/only-latest-filter.component.ts","../../ecosystem/application-plugins/only-latest-filter/only-latest-filter.component.html","../../ecosystem/application-plugins/appState.pipe.ts","../../ecosystem/application-plugins/apps-to-update-remotes-select.component.ts","../../ecosystem/application-plugins/apps-to-update-remotes-select.component.html","../../ecosystem/application-plugins/plugin-list.service.ts","../../ecosystem/application-plugins/plugin-list-item.component.ts","../../ecosystem/application-plugins/plugin-list-item.component.html","../../ecosystem/application-plugins/plugin-list.component.ts","../../ecosystem/application-plugins/plugin-list.component.html","../../ecosystem/application-plugins/install-plugin.component.ts","../../ecosystem/application-plugins/install-plugin.component.html","../../ecosystem/application-plugins/label-cell-renderer.component.ts","../../ecosystem/application-plugins/label-cell-renderer.component.html","../../ecosystem/application-plugins/orphaned-status-cell-renderer.component.ts","../../ecosystem/application-plugins/orphaned-status-cell-renderer.component.html","../../ecosystem/application-plugins/update-plugin-of-app/update-plugin-of-app.component.ts","../../ecosystem/application-plugins/update-plugin-of-app/update-plugin-of-app.component.html","../../ecosystem/application-plugins/application-plugin-readme.component.ts","../../ecosystem/application-plugins/application-plugin-readme.component.html","../../ecosystem/application-plugins/application-plugins.component.ts","../../ecosystem/application-plugins/application-plugins.component.html","../../ecosystem/application-plugins/application-plugins.guard.ts","../../ecosystem/application-plugins/application-plugins.module.ts","../../ecosystem/application-plugins/c8y-ngx-components-ecosystem-application-plugins.ts"],"sourcesContent":["import { Component } from '@angular/core';\nimport { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { ApplicationPlugin, gettext, C8yTranslatePipe } from '@c8y/ngx-components';\nimport { ListFiltersComponent } from '@c8y/ngx-components/ecosystem/shared';\n\n@Component({\n selector: 'c8y-only-latest-filter',\n templateUrl: './only-latest-filter.component.html',\n imports: [FormsModule, ReactiveFormsModule, C8yTranslatePipe]\n})\nexport class OnlyLatestFilterComponent {\n onlyLatestFormControl: FormControl;\n private readonly filterLabel = gettext('Only latest versions');\n\n constructor(public filterComponent: ListFiltersComponent) {\n this.onlyLatestFormControl = new FormControl(true);\n (this.filterComponent.formGroup.controls.custom as FormGroup).addControl(\n this.filterLabel,\n this.onlyLatestFormControl\n );\n filterComponent.customFilters.set(\n this.filterLabel,\n (plugin: ApplicationPlugin, enabled: boolean) => {\n return !enabled || !!plugin.tags?.includes('latest');\n }\n );\n }\n}\n","<label class=\"c8y-switch\">\n <input\n type=\"checkbox\"\n checked=\"checked\"\n [formControl]=\"onlyLatestFormControl\"\n />\n <span></span>\n {{ 'Show only latest version of plugins' | translate }}\n</label>\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { IApplication } from '@c8y/client';\nimport { ApplicationState, EcosystemService } from '@c8y/ngx-components/ecosystem/shared';\n@Pipe({\n name: 'appState',\n pure: true\n})\nexport class AppStatePipe implements PipeTransform {\n constructor(private ecosystemService: EcosystemService) {}\n\n transform(\n app: IApplication,\n arg: keyof ApplicationState\n ): ApplicationState[keyof ApplicationState] {\n const appState = this.ecosystemService.getAppState(app);\n return appState[arg];\n }\n}\n","import { Component, Input, OnDestroy, OnInit } from '@angular/core';\nimport {\n WizardConfig,\n WizardModalService,\n gettext,\n IconDirective,\n C8yTranslateDirective,\n FilterInputComponent,\n ListGroupComponent,\n ListItemComponent,\n ListItemCheckboxComponent,\n ListItemIconComponent,\n AppIconComponent,\n EmptyStateComponent,\n C8yTranslatePipe,\n HumanizeAppNamePipe\n} from '@c8y/ngx-components';\nimport { EcosystemService } from '@c8y/ngx-components/ecosystem/shared';\nimport { ApplicationType, IApplication } from '@c8y/client';\nimport { ModalOptions, BsModalRef } from 'ngx-bootstrap/modal';\nimport { UpdateType } from './apps-to-update-remotes-select.model';\nimport { Subject, BehaviorSubject, combineLatest, Observable, of } from 'rxjs';\nimport { map, takeUntil } from 'rxjs/operators';\nimport { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common';\nimport { AppStatePipe } from './appState.pipe';\n\n@Component({\n templateUrl: './apps-to-update-remotes-select.component.html',\n selector: 'c8y-apps-to-update-remotes-select',\n imports: [\n IconDirective,\n C8yTranslateDirective,\n FilterInputComponent,\n NgIf,\n ListGroupComponent,\n NgFor,\n ListItemComponent,\n NgClass,\n ListItemCheckboxComponent,\n ListItemIconComponent,\n AppIconComponent,\n EmptyStateComponent,\n C8yTranslatePipe,\n AsyncPipe,\n HumanizeAppNamePipe,\n AppStatePipe\n ]\n})\nexport class AppsToUpdateRemotesSelectComponent implements OnInit, OnDestroy {\n @Input() apps: IApplication[];\n @Input() updateType: UpdateType;\n @Input() pluginName: string;\n @Input() appsDisabled: Set<IApplication['id']>;\n private destroy$ = new Subject<void>();\n filterTerm$: BehaviorSubject<string> = new BehaviorSubject('');\n filteredApps$: Observable<IApplication[]> = new BehaviorSubject([]);\n textConfig: {\n header: string;\n applyButton: string;\n };\n appsToUpdateRemotes: IApplication[] = [];\n result: Promise<IApplication[]> = new Promise((resolve, reject) => {\n this._update = resolve;\n this._cancel = reject;\n });\n private _update: (apps: IApplication[]) => void;\n private _cancel: (reason?: any) => void;\n\n constructor(\n private bsModalRef: BsModalRef,\n private wizardModalService: WizardModalService,\n private ecosystemService: EcosystemService\n ) {}\n\n ngOnInit(): void {\n this.filteredApps$ = combineLatest([of(this.apps), this.filterTerm$]).pipe(\n map(([apps, filterTerm]) =>\n filterTerm.trim().length === 0\n ? apps\n : apps.filter((application: IApplication) =>\n this.ecosystemService.filterContainString(application.name, filterTerm)\n )\n )\n );\n this.textConfig =\n this.updateType === 'install'\n ? {\n header: gettext('Select applications to install the plugin to'),\n applyButton: gettext('Install')\n }\n : {\n header: gettext('Select applications to uninstall the plugin from'),\n applyButton: gettext('Uninstall')\n };\n }\n\n cancel() {\n this.bsModalRef.hide();\n this._cancel();\n }\n\n setSelectedApps(selected: boolean, app: IApplication) {\n selected\n ? this.appsToUpdateRemotes.push(app)\n : (this.appsToUpdateRemotes = this.appsToUpdateRemotes.filter(\n application => app.key !== application.key\n ));\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n async duplicateApp() {\n const wizardConfig: WizardConfig = {\n headerText: gettext('Duplicate application'),\n headerIcon: 'c8y-copy'\n };\n\n const initialState: Record<string, unknown> = {\n wizardConfig,\n componentInitialState: {\n noBackButton: true\n },\n id: 'duplicateApplication'\n };\n\n const modalOptions: ModalOptions = { initialState };\n\n const modalRef = this.wizardModalService.show(modalOptions);\n modalRef.content.onClose.pipe(takeUntil(this.destroy$)).subscribe(async () => {\n this.apps = await this.getOwnedHostedApps();\n this.ngOnInit();\n });\n }\n\n async apply() {\n this._update(this.appsToUpdateRemotes);\n this.bsModalRef.hide();\n }\n\n private async getOwnedHostedApps(): Promise<IApplication[]> {\n return (await this.ecosystemService.getWebApplications()).filter(\n app => this.ecosystemService.isOwner(app) && app.type !== ApplicationType.EXTERNAL\n );\n }\n}\n","<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'c8y-modules'\"></i>\n <div class=\"modal-title h4\" id=\"modal-title\" translate>Custom applications</div>\n </div>\n <div class=\"inner-scroll\" id=\"modal-body\">\n <div class=\"p-16 text-center separator-bottom sticky-top bg-component\">\n <p class=\"text-medium\">\n {{ textConfig.header | translate }}\n </p>\n <c8y-filter (onSearch)=\"filterTerm$.next($event)\"></c8y-filter>\n </div>\n <c8y-list-group *ngIf=\"apps.length; else emptyList\">\n <c8y-li\n [ngClass]=\"{ disabled: updateType === 'install' && appsDisabled.has(app.id) }\"\n *ngFor=\"let app of filteredApps$ | async\"\n data-cy=\"apps-to-update-remotes-select--applications-list\"\n >\n <c8y-li-checkbox (onSelect)=\"setSelectedApps($event, app)\" data-cy=\"apps-to-update-remotes-select--app-checkbox\"></c8y-li-checkbox>\n <c8y-li-icon class=\"p-l-0 icon-32\">\n <c8y-app-icon\n class=\"list-group-icon\"\n [app]=\"app\"\n [contextPath]=\"app.contextPath\"\n [name]=\"app.name\"\n ></c8y-app-icon>\n </c8y-li-icon>\n <div class=\"d-flex\">\n <div class=\"p-r-8\">\n <p class=\"text-medium\" [innerText]=\"app | humanizeAppName | async\"></p>\n <p class=\"small text-muted\">{{ app.description }}</p>\n </div>\n <span class=\"label m-l-auto a-s-start\" [ngClass]=\"app | appState: 'class'\">\n {{ app | appState: 'label' | translate }}\n </span>\n </div>\n </c8y-li>\n </c8y-list-group>\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 >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ textConfig.applyButton | translate }}\"\n [disabled]=\"appsToUpdateRemotes.length === 0\"\n (click)=\"apply()\"\n >\n {{ textConfig.applyButton | translate }}\n </button>\n </div>\n</div>\n<ng-template #emptyList>\n <c8y-ui-empty-state\n [icon]=\"'c8y-modules'\"\n [title]=\"'No custom applications available.' | translate\"\n *ngIf=\"updateType !== 'install'\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n <ng-container *ngIf=\"updateType === 'install'\">\n <c8y-ui-empty-state\n [icon]=\"'c8y-modules'\"\n [title]=\"'No custom applications available.' | translate\"\n [subtitle]=\"'Create a custom application by duplicating an existing one.' | translate\"\n [horizontal]=\"true\"\n >\n <button\n class=\"btn btn-sm btn-default m-t-8\"\n title=\"{{ 'Duplicate' | translate }}\"\n (click)=\"duplicateApp()\"\n >\n {{ 'Duplicate' | translate }}\n </button>\n </c8y-ui-empty-state>\n </ng-container>\n</ng-template>\n","import { inject, Injectable } from '@angular/core';\nimport {\n AlertService,\n ApplicationPlugin,\n GainsightService,\n gettext,\n HumanizeAppNamePipe,\n PluginsService\n} from '@c8y/ngx-components';\nimport { UpdateType } from './apps-to-update-remotes-select.model';\nimport { ApplicationType, IApplication } from '@c8y/client';\nimport { AppsToUpdateRemotesSelectComponent } from './apps-to-update-remotes-select.component';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslateService } from '@ngx-translate/core';\nimport { pick } from 'lodash-es';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport {\n EcosystemService,\n PRODUCT_EXPERIENCE_ECOSYSTEM\n} from '@c8y/ngx-components/ecosystem/shared';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class PluginListService {\n CURRENT_LOCATION = location.href;\n updatingPluginId: Record<UpdateType, string> = { install: '', uninstall: '' };\n private appsDisabled: Set<IApplication['id']> = new Set<IApplication['id']>();\n\n private gainsightService = inject(GainsightService);\n private pluginsService = inject(PluginsService);\n private alertService = inject(AlertService);\n private ecosystemService = inject(EcosystemService);\n private humanizeAppNamePipe = inject(HumanizeAppNamePipe);\n private translateService = inject(TranslateService);\n private bsModalService = inject(BsModalService);\n\n async updateAppRemotes(\n plugin: ApplicationPlugin,\n updateType: UpdateType,\n pluginPackage: IApplication\n ) {\n this.updatingPluginId[updateType] = plugin?.id;\n let initialState: Pick<\n AppsToUpdateRemotesSelectComponent,\n 'apps' | 'updateType' | 'pluginName' | 'appsDisabled'\n >;\n try {\n const apps = await this.getAppsForUpdate(plugin, updateType);\n\n initialState = {\n apps,\n updateType,\n pluginName: plugin.name,\n appsDisabled: this.appsDisabled\n };\n } catch (e) {\n this.alertService.addServerFailure(e);\n this.updatingPluginId[updateType] = '';\n return;\n }\n\n let selectedApps: IApplication[];\n try {\n selectedApps = await this.selectApps(initialState);\n if (!selectedApps) {\n this.updatingPluginId[updateType] = '';\n return;\n }\n } catch {\n // unreached\n }\n\n if (updateType === 'install') {\n const isArchived = await this.ecosystemService.verifyArchived([plugin]);\n if (!isArchived) {\n this.updatingPluginId[updateType] = '';\n return;\n }\n\n const licensesVerifiedByUser = await this.ecosystemService.verifyLicenses([plugin]);\n if (!licensesVerifiedByUser) {\n this.updatingPluginId[updateType] = '';\n return;\n }\n }\n\n for (const app of selectedApps) {\n try {\n if (updateType === 'install') {\n const versionIsCompatible = await this.ecosystemService.verifyPluginVersionsCompatibility(\n [plugin],\n app\n );\n\n if (!versionIsCompatible) {\n continue;\n }\n }\n\n await this.handleRemotesUpdate(app, plugin, updateType, pluginPackage);\n const humanizedAppName = await firstValueFrom(this.humanizeAppNamePipe.transform(app));\n const successText =\n updateType === 'install'\n ? this.translateService.instant(\n gettext('Plugin installed to application \"{{ appName }}\".'),\n {\n appName: humanizedAppName\n }\n )\n : this.translateService.instant(\n gettext('Plugin uninstalled from application \"{{ appName }}\".'),\n { appName: humanizedAppName }\n );\n this.alertService.success(successText);\n this.onUpdateEventHandleGS(plugin, app, updateType);\n } catch (error) {\n this.onUpdateEventHandleGS(plugin, app, updateType, error);\n }\n }\n this.updatingPluginId[updateType] = '';\n }\n\n async getAppsForUpdate(plugin: ApplicationPlugin, updateType: UpdateType) {\n let apps = (await this.ecosystemService.getWebApplications()).filter(\n app => this.ecosystemService.isOwner(app) && app.type !== ApplicationType.EXTERNAL\n );\n\n if (updateType === 'install') {\n this.appsDisabled.clear();\n for (const app of apps) {\n if (this.isPluginInstalledInApp(plugin, app)) {\n this.appsDisabled.add(app.id);\n }\n }\n }\n\n if (updateType === 'uninstall') {\n const installedApps: IApplication[] = [];\n for (const app of apps) {\n if (this.isPluginInstalledInApp(plugin, app)) {\n installedApps.push(app);\n }\n }\n apps = installedApps;\n }\n return apps;\n }\n\n private onUpdateEventHandleGS(\n plugin: ApplicationPlugin,\n app: IApplication,\n updateType: UpdateType,\n error?: unknown\n ) {\n const pluginCustomEventInfo = pick(plugin, [\n 'name',\n 'contextPath',\n 'module',\n 'version',\n 'type',\n 'id'\n ] satisfies (keyof ApplicationPlugin)[]);\n\n const gsEventResult =\n updateType === 'install'\n ? PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_INSTALLED\n : PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_REMOVED;\n\n const eventData = {\n component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.PLUGIN_LIST,\n result: error || gsEventResult,\n url: this.CURRENT_LOCATION,\n ...pluginCustomEventInfo,\n targetApplicationName: app.name,\n targetApplicationContextPath: app.contextPath,\n ...(error && { error })\n };\n\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.PACKAGE_PLUGINS,\n eventData\n );\n }\n\n private isPluginInstalledInApp(plugin: ApplicationPlugin, app: IApplication): boolean {\n const appRemotes = this.pluginsService.getMFRemotes(app) || {};\n\n for (const [remoteName, modules] of Object.entries(appRemotes)) {\n const pluginFromThisPackageIsInstalled =\n this.getPluginContextPathWithoutVersion(remoteName) === plugin.contextPath;\n const specificPluginModuleIsInstalled = modules.some(module => module === plugin.module);\n if (pluginFromThisPackageIsInstalled && specificPluginModuleIsInstalled) {\n return true;\n }\n }\n return false;\n }\n\n private getPluginContextPathWithoutVersion(remote: string) {\n return remote.split('@')[0];\n }\n\n private async handleRemotesUpdate(\n application: IApplication,\n plugin: ApplicationPlugin,\n updateType: UpdateType,\n pluginPackage: IApplication\n ) {\n try {\n // When remotes object is not set in the configuration object of an application.\n // Fallback to setInitialRemotes is triggered.\n const { remotes, excludedRemotes } = await (updateType === 'install'\n ? this.pluginsService.addRemotes(application, plugin)\n : this.pluginsService.removeRemotes(\n application,\n this.getAllPluginsToRemove(plugin, pluginPackage)\n ));\n if (!application.config) {\n application.config = {};\n }\n application.config.remotes = remotes;\n application.config.excludedRemotes = excludedRemotes;\n const actualRemotes = this.pluginsService.getMFRemotes(application);\n return actualRemotes;\n } catch (er) {\n if (er) {\n this.alertService.addServerFailure(er);\n }\n throw er;\n }\n }\n\n private getAllPluginsToRemove(\n plugin: ApplicationPlugin,\n pluginPackage: IApplication\n ): ApplicationPlugin[] {\n return pluginPackage.applicationVersions.map(av => ({\n id: `${plugin.contextPath}@${av.version}/${plugin.module}`,\n idLatest: `${plugin.contextPath}/${plugin.module}`,\n module: plugin.module,\n path: plugin.path\n }));\n }\n\n private async selectApps(\n initialState: Pick<\n AppsToUpdateRemotesSelectComponent,\n 'apps' | 'updateType' | 'pluginName' | 'appsDisabled'\n >\n ): Promise<IApplication[]> {\n try {\n return await (\n this.bsModalService.show(AppsToUpdateRemotesSelectComponent, {\n class: 'modal-sm',\n ariaDescribedby: 'modal-body',\n ariaLabelledBy: 'modal-title',\n initialState,\n ignoreBackdropClick: true,\n keyboard: false\n }).content as AppsToUpdateRemotesSelectComponent\n ).result;\n } catch (er) {\n return;\n }\n }\n}\n","import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';\nimport {\n ApplicationPlugin,\n PackageType,\n PluginsService,\n ListItemCheckboxComponent,\n ListItemIconComponent,\n IconDirective,\n C8yTranslateDirective\n} from '@c8y/ngx-components';\nimport { NgIf, NgClass } from '@angular/common';\nimport { TranslatePackageLabelPipe } from '@c8y/ngx-components/ecosystem/shared';\n\n@Component({\n selector: 'c8y-plugin-list-item',\n templateUrl: './plugin-list-item.component.html',\n imports: [\n NgIf,\n ListItemCheckboxComponent,\n ListItemIconComponent,\n NgClass,\n IconDirective,\n C8yTranslateDirective,\n TranslatePackageLabelPipe\n ]\n})\nexport class PluginListItemComponent implements OnInit {\n @Input() plugin: ApplicationPlugin;\n @Input() selectable: boolean;\n @Input() hideSource = false;\n @Output() isItemSelected: EventEmitter<boolean> = new EventEmitter();\n\n packageType: PackageType = PackageType.UNKNOWN;\n\n readonly PACKAGE_TYPE = PackageType;\n\n constructor(private pluginService: PluginsService) {}\n\n ngOnInit() {\n this.packageType = this.pluginService.getPackageType(this.plugin.originApp);\n }\n\n onChange(event) {\n this.plugin.selected = !this.plugin.selected;\n this.isItemSelected.next(event);\n }\n}\n","<c8y-li-checkbox\n class=\"p-r-16 p-l-0\"\n (change)=\"onChange($event.target.checked)\"\n *ngIf=\"selectable\"\n [disabled]=\"plugin.installed\"\n [selected]=\"plugin.selected\"\n></c8y-li-checkbox>\n<c8y-li-icon class=\"p-l-0 text-center\">\n <i class=\"c8y-plugin-icon\">\n <span>{{ plugin.name?.substr(0, 2) }}</span>\n </i>\n</c8y-li-icon>\n<div class=\"p-relative flex-grow\">\n <div [ngClass]=\"{ 'p-r-8': selectable }\">\n <p>\n <span class=\"text-medium\">{{ plugin.name }}</span>\n <em class=\"text-muted small m-l-8\">{{ plugin.version }}</em>\n <span *ngIf=\"plugin.installed\">\n <i\n class=\"text-success\"\n [c8yIcon]=\"'check-circle'\"\n ></i>\n <em\n class=\"text-muted small\"\n translate\n >\n Installed`plugins`\n </em>\n </span>\n </p>\n <p class=\"small l-h-tight\">{{ plugin.description }}</p>\n </div>\n\n <span\n class=\"tag tag--info a-s-start m-t-8\"\n *ngIf=\"selectable && !hideSource\"\n >\n {{ plugin.contextPath }}\n </span>\n\n <span\n class=\"tag a-s-start m-t-8 m-l-4\"\n [ngClass]=\"{\n 'tag--default': packageType === PACKAGE_TYPE.COMMUNITY,\n 'tag--primary': packageType === PACKAGE_TYPE.OFFICIAL\n }\"\n >\n {{ plugin.originApp?.label || plugin.originApp?.manifest?.label | translatePackageLabel }}\n </span>\n</div>\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { IApplication } from '@c8y/client';\nimport {\n ApplicationPlugin,\n ListGroupComponent,\n ListItemComponent,\n IconDirective,\n C8yTranslateDirective,\n EmptyStateComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\n\nimport { Observable } from 'rxjs';\n\nimport { PluginListService } from './plugin-list.service';\nimport { UpdateType } from './apps-to-update-remotes-select.model';\nimport { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common';\nimport { PluginListItemComponent } from './plugin-list-item.component';\n\n@Component({\n selector: 'c8y-plugin-list',\n templateUrl: './plugin-list.component.html',\n imports: [\n ListGroupComponent,\n NgIf,\n NgFor,\n ListItemComponent,\n NgClass,\n PluginListItemComponent,\n IconDirective,\n C8yTranslateDirective,\n EmptyStateComponent,\n C8yTranslatePipe,\n AsyncPipe\n ]\n})\nexport class PluginListComponent {\n CURRENT_LOCATION = location.href;\n\n @Input() plugins$: Observable<ApplicationPlugin[]>;\n @Input() emptyListText = '';\n @Input() selectable: boolean;\n @Input() hideSource = false;\n /**\n * Shows the install button for each plugin separately. Currently used in package-details view.\n */\n @Input() installable = false;\n @Input() package: IApplication;\n @Input() selectedPlugin: ApplicationPlugin;\n @Output() selectedItems: EventEmitter<ApplicationPlugin[]> = new EventEmitter();\n @Output() showOverview = new EventEmitter<ApplicationPlugin>();\n selectedPlugins: { [key: string]: ApplicationPlugin } = {};\n updatingPluginId: Record<UpdateType, string>;\n\n constructor(private pluginListService: PluginListService) {\n this.updatingPluginId = this.pluginListService.updatingPluginId;\n }\n\n updateSelectedItems(selected: boolean, plugin: ApplicationPlugin) {\n this.selectedPlugins[plugin.id] = selected ? plugin : undefined;\n const onlyInstalledPlugins = Object.values(this.selectedPlugins).filter(Boolean);\n this.selectedItems.emit(onlyInstalledPlugins);\n }\n\n showPluginOverview(plugin: ApplicationPlugin): void {\n if (plugin?.id === this.selectedPlugin?.id) {\n return;\n }\n this.showOverview.emit(plugin);\n }\n\n async installPlugin(plugin: ApplicationPlugin) {\n await this.pluginListService.updateAppRemotes(plugin, 'install', this.package);\n }\n\n async uninstallPlugin(plugin: ApplicationPlugin) {\n await this.pluginListService.updateAppRemotes(plugin, 'uninstall', this.package);\n }\n}\n","<c8y-list-group class=\"bg-inherit\">\n <ng-container *ngIf=\"(plugins$ | async)?.length !== 0; else emptyList\">\n <ng-container *ngFor=\"let plugin of plugins$ | async\">\n <c8y-li\n [ngClass]=\"{\n disabled: plugin.installed,\n selected: selectedPlugin?.id === plugin?.id\n }\"\n >\n <div class=\"d-flex fit-w\">\n <ng-container *ngIf=\"plugin.readmePath\">\n <button\n class=\"c8y-list__item__btn d-flex fit-w gap-4\"\n title=\"{{ 'Details' | translate }}\"\n (click)=\"showPluginOverview(plugin)\"\n >\n <c8y-plugin-list-item\n class=\"d-contents\"\n (isItemSelected)=\"updateSelectedItems($event, plugin)\"\n [plugin]=\"plugin\"\n [selectable]=\"selectable\"\n [hideSource]=\"hideSource\"\n ></c8y-plugin-list-item>\n <i\n class=\"icon-24 m-l-auto a-s-center\"\n c8yIcon=\"forward\"\n ></i>\n </button>\n </ng-container>\n <ng-container *ngIf=\"!plugin.readmePath\">\n <c8y-plugin-list-item\n class=\"d-contents\"\n (isItemSelected)=\"updateSelectedItems($event, plugin)\"\n [plugin]=\"plugin\"\n [selectable]=\"selectable\"\n [hideSource]=\"hideSource\"\n ></c8y-plugin-list-item>\n </ng-container>\n </div>\n <div class=\"p-l-40 m-t-4 d-flex flex-wrap gap-8\">\n <button\n class=\"btn btn-danger btn-sm\"\n title=\"{{ 'Uninstall plugin' | translate }}\"\n *ngIf=\"installable\"\n (click)=\"uninstallPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.uninstall }\"\n [disabled]=\"updatingPluginId.uninstall && plugin.id === updatingPluginId.uninstall\"\n data-cy=\"plugin-list--uninstall-plugin-button\"\n translate\n >\n Uninstall plugin\n </button>\n <button\n class=\"btn btn-default btn-sm m-0\"\n title=\"{{ 'Install plugin' | translate }}\"\n *ngIf=\"installable\"\n (click)=\"installPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.install }\"\n [disabled]=\"updatingPluginId.install && plugin.id === updatingPluginId.install\"\n data-cy=\"plugin-list--install-plugin-button\"\n translate\n >\n Install plugin\n </button>\n </div>\n </c8y-li>\n </ng-container>\n </ng-container>\n</c8y-list-group>\n<ng-template #emptyList>\n <c8y-ui-empty-state\n [icon]=\"'plugin'\"\n [title]=\"emptyListText | translate\"\n [horizontal]=\"true\"\n *ngIf=\"emptyListText\"\n ></c8y-ui-empty-state>\n</ng-template>\n","import { Component, Input } from '@angular/core';\nimport {\n ApplicationPlugin,\n BottomDrawerRef,\n PluginsService,\n C8yTranslateDirective,\n IconDirective,\n EmptyStateComponent,\n C8yTranslatePipe,\n MarkdownToHtmlPipe\n} from '@c8y/ngx-components';\nimport {\n defaultPackageTypes,\n EcosystemService,\n FilterableAppOrPlugin,\n FilterPipe,\n ListFiltersComponent,\n ArchivedFilterComponent\n} from '@c8y/ngx-components/ecosystem/shared';\nimport { BehaviorSubject, map, Observable } from 'rxjs';\nimport { OnlyLatestFilterComponent } from './only-latest-filter/only-latest-filter.component';\nimport { PluginListComponent } from './plugin-list.component';\nimport { NgIf, AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'c8y-install-plugin',\n host: { class: 'd-contents' },\n templateUrl: './install-plugin.component.html',\n imports: [\n C8yTranslateDirective,\n ListFiltersComponent,\n ArchivedFilterComponent,\n OnlyLatestFilterComponent,\n PluginListComponent,\n NgIf,\n IconDirective,\n EmptyStateComponent,\n C8yTranslatePipe,\n AsyncPipe,\n MarkdownToHtmlPipe\n ]\n})\nexport class InstallPluginComponent {\n @Input() plugins$: Observable<FilterableAppOrPlugin[]>;\n filteredPlugins$: Observable<FilterableAppOrPlugin[]> = new BehaviorSubject([]);\n selectedPlugins: ApplicationPlugin[] = [];\n packageTypes = defaultPackageTypes;\n result: Promise<ApplicationPlugin[]> = new Promise((resolve, reject) => {\n this._install = resolve;\n this._cancel = reject;\n });\n onlyLatestPluginVersion = true;\n selectedPlugin: ApplicationPlugin;\n pluginBaseUrl: string;\n pluginMarkdown: string;\n\n private _install;\n private _cancel;\n\n constructor(\n private bottomDrawerRef: BottomDrawerRef<InstallPluginComponent>,\n private ecosystemService: EcosystemService,\n private pluginsService: PluginsService\n ) {}\n\n setFilterPipe(filterPipe: FilterPipe) {\n this.filteredPlugins$ = this.plugins$.pipe(\n map(plugins =>\n plugins.map(plugin => {\n plugin.filterProps = this.ecosystemService.getAppFilterProps(plugin.originApp);\n return plugin;\n })\n ),\n src => filterPipe(src)\n );\n }\n\n cancel() {\n this.bottomDrawerRef.close();\n this._cancel();\n }\n\n install() {\n this._install(this.selectedPlugins);\n this.bottomDrawerRef.close();\n }\n\n async showPluginOverview(plugin: ApplicationPlugin) {\n this.selectedPlugin = plugin;\n const baseUrl = `/apps/${plugin.id}/`;\n this.pluginBaseUrl = baseUrl;\n this.pluginMarkdown = await this.pluginsService.getReadmeFileContent(baseUrl);\n }\n}\n"," <div class=\"card-header gap-8 d-col p-l-24 p-r-24 separator-bottom flex-no-shrink\">\n <div\n class=\"card-title h4 text-center\"\n translate\n >\n Available plugins\n </div>\n <c8y-list-filters\n (filterPipeChange)=\"setFilterPipe($event)\"\n [packageTypes]=\"packageTypes\"\n >\n <c8y-archived-filter></c8y-archived-filter>\n <c8y-only-latest-filter></c8y-only-latest-filter>\n </c8y-list-filters>\n </div>\n <div class=\"d-grid grid__col--5-7--md min-height-0 flex-grow\">\n <c8y-plugin-list\n class=\"inner-scroll bg-level-1\"\n (selectedItems)=\"selectedPlugins = $event\"\n [emptyListText]=\"'No matching plugins' | translate\"\n [plugins$]=\"filteredPlugins$\"\n [selectable]=\"true\"\n [selectedPlugin]=\"selectedPlugin\"\n (showOverview)=\"showPluginOverview($event)\"\n ></c8y-plugin-list>\n <div class=\"inner-scroll bg-component\">\n <div class=\"card-header separator sticky-top bg-inherit\"\n *ngIf=\"pluginMarkdown\">\n <button\n class=\"m-l-auto btn-clean\"\n title=\"{{ 'Close' | translate }}\"\n type=\"button\"\n (click)=\"selectedPlugin = null; pluginMarkdown = null\"\n >\n <i c8yIcon=\"times\"></i>\n </button>\n </div>\n <div class=\"card-block p-l-24 p-r-24\">\n <div\n class=\"markdown-content\"\n *ngIf=\"pluginMarkdown\"\n [innerHTML]=\"pluginMarkdown | markdownToHtml: { baseUrl: pluginBaseUrl } | async\"\n ></div>\n <c8y-ui-empty-state\n [icon]=\"'user-manual'\"\n [title]=\"'No plugin selected' | translate\"\n [subtitle]=\"\n 'Select a plugin from the list to view its documentation.' | translate\"\n *ngIf=\"!pluginMarkdown\"\n [horizontal]=\"true\"\n >\n <p>\n <small >\n {{ 'Documentation availability varies by plugin.' | translate }}\n </small>\n </p>\n </c8y-ui-empty-state>\n </div>\n </div>\n </div>\n <div class=\"text-center card-footer p-24 separator\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n data-cy=\"install-plugin--cancel-button\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Install' | translate }}\"\n type=\"button\"\n (click)=\"install()\"\n [disabled]=\"selectedPlugins.length === 0\"\n data-cy=\"install-plugin--install-button\"\n >\n {{ 'Install' | translate }}\n <span\n class=\"badge\"\n *ngIf=\"selectedPlugins.length as length\"\n >\n {{ length }}\n </span>\n </button>\n </div>\n\n","import { Component } from '@angular/core';\nimport { CellRendererContext } from '@c8y/ngx-components';\n\n@Component({\n selector: 'c8y-label-cell-renderer',\n templateUrl: './label-cell-renderer.component.html'\n})\nexport class LabelCellRendererComponent {\n constructor(public context: CellRendererContext) {}\n}\n","<span class=\"label label-info\">{{ context.value }}</span>\n","import { Component } from '@angular/core';\nimport {\n ApplicationPluginStatus,\n CellRendererContext,\n gettext,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { NgIf, NgClass } from '@angular/common';\n\n@Component({\n selector: 'c8y-orphaned-status-cell-renderer',\n templateUrl: './orphaned-status-cell-renderer.component.html',\n imports: [NgIf, NgClass, C8yTranslatePipe]\n})\nexport class OrphanedStatusCellRendererComponent {\n label: { value: string; class: string; text: string } | null;\n constructor(public context: CellRendererContext) {\n this.label = this.getLabel(context.value);\n }\n\n private getLabel(statusValue: ApplicationPluginStatus): {\n value: string;\n class: string;\n text: string;\n } | null {\n switch (statusValue) {\n case ApplicationPluginStatus.OUTDATED:\n return {\n value: statusValue,\n text: gettext('OUTDATED`plugin status`'),\n class: 'label-warning'\n };\n case ApplicationPluginStatus.ORPHANED:\n return {\n value: statusValue,\n text: gettext('ORPHANED`plugin status`'),\n class: 'label-danger'\n };\n case ApplicationPluginStatus.REVOKED:\n return {\n value: statusValue,\n text: gettext('REVOKED`plugin status`'),\n class: 'label-danger'\n };\n case ApplicationPluginStatus.LATEST:\n return {\n value: statusValue,\n text: gettext('LATEST`plugin status`'),\n class: 'label-success'\n };\n case ApplicationPluginStatus.AUTO:\n return {\n value: statusValue,\n text: gettext('AUTO`plugin status`'),\n class: 'label-success'\n };\n default:\n return null;\n }\n }\n}\n","<span\n *ngIf=\"label\"\n class=\"label\"\n [ngClass]=\"label.class\"\n [title]=\"label.text | translate\"\n>\n {{ label.text | translate }}\n</span>\n","import { Component } from '@angular/core';\nimport { ApplicationRemotePlugins, IApplication, IApplicationVersion } from '@c8y/client';\nimport {\n AlertService,\n ApplicationPlugin,\n GainsightService,\n PluginsService,\n gettext,\n IconDirective,\n C8yTranslateDirective,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { uniq } from 'lodash-es';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport {\n EcosystemService,\n PRODUCT_EXPERIENCE_ECOSYSTEM,\n PackageVersionSelectComponent,\n PackageChangelogComponent\n} from '@c8y/ngx-components/ecosystem/shared';\nimport { NgIf } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\n@Component({\n selector: 'c8y-update-plugin-of-app',\n templateUrl: './update-plugin-of-app.component.html',\n imports: [\n IconDirective,\n NgIf,\n C8yTranslateDirective,\n PackageVersionSelectComponent,\n FormsModule,\n PackageChangelogComponent,\n C8yTranslatePipe\n ]\n})\nexport class UpdatePluginOfAppComponent {\n CURRENT_LOCATION = location.href;\n\n app: IApplication;\n plugin: ApplicationPlugin;\n downgrade: boolean;\n result: Promise<void> = new Promise((resolve, reject) => {\n this._install = resolve;\n this._cancel = reject;\n });\n applicationVersion: IApplicationVersion;\n updateAll = true;\n\n private _install: () => void;\n private _cancel: (reason?: any) => void;\n\n constructor(\n private bsModalRef: BsModalRef,\n private pluginsService: PluginsService,\n private alert: AlertService,\n private ecosystemService: EcosystemService,\n private gainsightService: GainsightService\n ) {}\n\n async update() {\n const remotes = this.pluginsService.getMFRemotes(this.app);\n const oldRemotePath = `${this.plugin.contextPath}@${this.plugin.version}`;\n const newRemotePath = `${this.plugin.contextPath}@${this.applicationVersion.version}`;\n let oldRemoteModules = [...(remotes[oldRemotePath] || [])];\n if (!oldRemoteModules.length) {\n this.alert.warning(gettext('Could not change the version of plugin.'));\n this.cancel();\n return;\n }\n\n const isArchived = await this.ecosystemService.verifyArchived([this.plugin]);\n if (!isArchived) {\n this.alert.warning(gettext('Plugin update aborted by user.'));\n this.cancel();\n return;\n }\n\n const result = await this.ecosystemService.verifyLicenses([this.plugin]);\n if (!result) {\n this.alert.warning(gettext('Plugin update aborted by user.'));\n this.cancel();\n return;\n }\n\n let remoteModulesOfNewVersion = [...(remotes[newRemotePath] || [])];\n let olderVersions: ApplicationRemotePlugins = {};\n if (this.updateAll) {\n olderVersions = Object.keys(remotes)\n .filter(key => key.startsWith(`${this.plugin.contextPath}@`))\n .reduceRight((prev, curr) => {\n prev[curr] = undefined;\n return prev;\n }, {});\n\n oldRemoteModules = Object.keys(olderVersions)\n .map(version => remotes[version])\n .reduceRight((prev, curr) => {\n prev.push(...curr);\n return prev;\n }, []);\n remoteModulesOfNewVersion.push(...oldRemoteModules);\n } else {\n remoteModulesOfNewVersion.push(this.plugin.module);\n olderVersions[oldRemotePath] = oldRemoteModules.filter(\n module => module !== this.plugin.module\n );\n if (!olderVersions[oldRemotePath].length) {\n olderVersions[oldRemotePath] = undefined;\n }\n }\n\n remoteModulesOfNewVersion = uniq(remoteModulesOfNewVersion);\n const newRemotes: ApplicationRemotePlugins = {\n ...remotes,\n ...olderVersions,\n [newRemotePath]: remoteModulesOfNewVersion\n };\n\n try {\n await this.pluginsService.updateRemotesInAppConfig(this.app, newRemotes);\n this.alert.success(gettext(`Switched the version of plugin.`));\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.INSTALLED_PLUGINS,\n {\n component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.UPDATE_PLUGIN_OF_APP,\n action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.CHANGE_PLUGIN_VERSION,\n result: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_VERSION_CHANGED,\n url: this.CURRENT_LOCATION\n }\n );\n } catch (e) {\n this.alert.addServerFailure(e);\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.INSTALLED_PLUGINS,\n {\n component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.UPDATE_PLUGIN_OF_APP,\n action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.CHANGE_PLUGIN_VERSION,\n result: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.SERVER_FAILURE,\n url: this.CURRENT_LOCATION\n }\n );\n }\n\n this.bsModalRef.hide();\n this._install();\n }\n\n cancel() {\n this.bsModalRef.hide();\n this._cancel();\n }\n}\n","<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'installing-updates'\"></i>\n <h4\n id=\"modal-title\"\n *ngIf=\"!downgrade\"\n translate\n >\n Update plugin\n </h4>\n <h4\n id=\"modal-title\"\n *ngIf=\"downgrade\"\n translate\n >\n Downgrade plugin\n </h4>\n </div>\n <div\n class=\"inner-scroll\"\n id=\"modal-body\"\n >\n <div class=\"card-block p-r-24 p-l-24 sticky-top bg-component separator-bottom\">\n <c8y-package-version-select\n [packageContextPath]=\"plugin?.contextPath\"\n [(ngModel)]=\"applicationVersion\"\n ></c8y-package-version-select>\n <div\n class=\"alert alert-info\"\n role=\"alert\"\n *ngIf=\"plugin?.version && plugin.version === applicationVersion?.version\"\n >\n <span\n translate\n ngNonBindable\n [translateParams]=\"applicationVersion\"\n >\n Select another version, as {{ version }} is currently used.\n </span>\n </div>\n <div class=\"form-group\">\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"updateAll\"\n />\n <span></span>\n <span\n translate\n ngNonBindable\n [translateParams]=\"plugin\"\n >\n Set version for all plugins using the same context path \"{{ contextPath }}\".\n </span>\n </label>\n </div>\n <p class=\"legend form-block\">\n {{ 'Change log' | translate }}\n </p>\n </div>\n <div class=\"card-block p-l-24 p-r-24\">\n\n <div\n class=\"markdown-content markdown-content--to-h3\"\n >\n <c8y-contents-changelog\n [package]=\"plugin.originApp\"\n [selectedVersion]=\"applicationVersion?.version\"\n [previousVersion]=\"plugin?.version\"\n ></c8y-contents-changelog>\n </div>\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 >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Set version' | translate }}\"\n [disabled]=\"!applicationVersion || plugin?.version === applicationVersion?.version\"\n (click)=\"update()\"\n >\n {{ 'Set version' | translate }}\n </button>\n </div>\n</div>","import { Component, inject, OnInit } from '@angular/core';\nimport {\n ApplicationPlugin,\n BottomDrawerRef,\n PluginsService,\n EmptyStateComponent,\n C8yTranslatePipe,\n MarkdownToHtmlPipe\n} from '@c8y/ngx-components';\nimport { NgIf, AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'c8y-application-plugin-readme',\n host: { class: 'd-contents' },\n templateUrl: './application-plugin-readme.component.html',\n imports: [NgIf, EmptyStateComponent, C8yTranslatePipe, AsyncPipe, MarkdownToHtmlPipe]\n})\nexport class ApplicationPluginReadmeComponent implements OnInit {\n pluginMarkdown: string;\n pluginBaseUrl: string;\n plugin: ApplicationPlugin;\n\n private bottomDrawerRef = inject(BottomDrawerRef);\n private pluginsService = inject(PluginsService);\n\n async ngOnInit() {\n const baseUrl = `/apps/${this.plugin.id}/`;\n this.pluginBaseUrl = baseUrl;\n this.pluginMarkdown = await this.pluginsService.getReadmeFileContent(baseUrl);\n }\n\n close() {\n this.bottomDrawerRef.close();\n }\n}\n","<div class=\"card-header gap-8 d-col p-l-24 p-r-24 separator-bottom flex-no-shrink\">\n <span class=\"card-title h4 text-center\">\n {{ plugin.name }}\n </span>\n</div>\n<div class=\"inner-scroll flex-grow\">\n <div\n class=\"markdown-content col-lg-8 p-24 m-l-auto m-r-auto\"\n style=\"float: none\"\n *ngIf=\"pluginMarkdown\"\n [innerHTML]=\"pluginMarkdown | markdownToHtml: { baseUrl: pluginBaseUrl } | async\"\n ></div>\n <div class=\"d-flex\">\n <c8y-ui-empty-state\n class=\"col-lg-3 col-sm-4 m-l-auto m-r-auto\"\n [icon]=\"'user-manual'\"\n [title]=\"'No README.md found for plugin' | translate\"\n [subtitle]=\"\n 'To view the contents of &quot;README&quot;, add the file &quot;README.md&quot; to the plugin.'\n | translate\n \"\n *ngIf=\"!pluginMarkdown\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </div>\n</div>\n<div class=\"text-center card-footer p-24 separator\">\n <button\n class=\"btn btn-default\"\n (click)=\"close()\"\n >\n {{ 'Close' | translate }}\n </button>\n</div>\n","import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { ApplicationRemotePlugins, IApplication } from '@c8y/client';\nimport {\n ActionControl,\n AlertService,\n ApplicationPlugin,\n ApplicationPluginStatus,\n BulkActionControl,\n Column,\n DataGridComponent,\n DisplayOptions,\n GainsightService,\n gettext,\n HeaderActionControl,\n Pagination,\n PluginsConfig,\n PluginsService,\n PluginsExportScopes,\n BottomDrawerService,\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n IconDirective,\n EmptyStateContextDirective,\n EmptyStateComponent,\n C8yTranslateDirective,\n C8yTranslatePipe,\n HumanizeAppNamePipe\n} from '@c8y/ngx-components';\nimport {\n EcosystemService,\n PRODUCT_EXPERIENCE_ECOSYSTEM\n} from '@c8y/ngx-components/ecosystem/shared';\nimport { pick } from 'lodash-es';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { BehaviorSubject, combineLatest, firstValueFrom, Observable, Subject } from 'rxjs';\nimport { map, shareReplay } from 'rxjs/operators';\nimport { InstallPluginComponent } from './install-plugin.component';\nimport { LabelCellRendererComponent } from './label-cell-renderer.component';\nimport { OrphanedStatusCellRendererComponent } from './orphaned-status-cell-renderer.component';\nimport { UpdatePluginOfAppComponent } from './update-plugin-of-app/update-plugin-of-app.component';\nimport { ApplicationPluginReadmeComponent } from './application-plugin-readme.component';\nimport { NgIf, NgClass, AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'c8y-app-plugins',\n templateUrl: './application-plugins.component.html',\n imports: [\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n NgIf,\n ActionBarItemComponent,\n NgClass,\n IconDirective,\n DataGridComponent,\n EmptyStateContextDirective,\n EmptyStateComponent,\n C8yTranslateDirective,\n C8yTranslatePipe,\n AsyncPipe,\n HumanizeAppNamePipe\n ]\n})\nexport class ApplicationPluginsComponent implements OnInit, OnDestroy {\n PRODUCT_EXPERIENCE = PRODUCT_EXPERIENCE_ECOSYSTEM;\n CURRENT_LOCATION = location.href;\n\n @Input() appId: string | number;\n @ViewChild(DataGridComponent, { static: false })\n dataGrid: DataGridComponent;\n\n remotePlugins$: BehaviorSubject<ApplicationRemotePlugins> = new BehaviorSubject({});\n allAvailablePlugins$: BehaviorSubject<ApplicationPlugin[]> = new BehaviorSubject([]);\n selfPlugins$: BehaviorSubject<ApplicationPlugin[]> = new BehaviorSubject([]);\n\n installedPlugins$: Observable<ApplicationPlugin[]> = combineLatest([\n this.remotePlugins$.pipe(map(remotes => PluginsService.convertInstalledRemotesToIds(remotes))),\n this.allAvailablePlugins$\n ]).pipe(\n map(([remotePlugins, allPlugins]) => this.getInstalledPlugins(allPlugins, remotePlugins)),\n shareReplay(1)\n );\n orphanedPlugins$ = this.installedPlugins$.pipe(\n map(plugins => plugins.filter(p => p.status === ApplicationPluginStatus.ORPHANED))\n );\n isStandard$ = combineLatest([this.installedPlugins$, this.selfPlugins$]).pipe(\n map(([installedPlugins, selfPlugins]) => {\n const manifestRemotes = this.app?.manifest?.remotes || {};\n // ensure that every installed plugin is a self plugin or a plugin from the manifest\n const allInstalledPluginsAreSelf = installedPlugins.every(\n p =>\n selfPlugins.some(selfPlugin => selfPlugin.id === p.id) ||\n (Array.isArray(manifestRemotes[p.contextPath]) &&\n manifestRemotes[p.contextPath].includes(p.module))\n );\n\n // ensure that every self plugin is installed\n const allSelfPluginsAreInstalled = selfPlugins.every(selfPlugin =>\n installedPlugins.some(p => p.id === selfPlugin.id)\n