UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

567 lines (558 loc) 91.7 kB
import * as i0 from '@angular/core'; import { Pipe, Component, Input, EventEmitter, Output, ViewChild, Injectable, NgModule } from '@angular/core'; import * as i2 from '@c8y/ngx-components'; import { gettext, PackageType, ApplicationPluginStatus, PluginsService, PluginsExportScopes, DataGridComponent, CoreModule, hookRoute, ViewContext } from '@c8y/ngx-components'; import * as i1 from '@c8y/ngx-components/ecosystem/shared'; import { PRODUCT_EXPERIENCE_ECOSYSTEM, defaultPackageTypes, SharedEcosystemModule } from '@c8y/ngx-components/ecosystem/shared'; import * as i1$2 from '@angular/router'; import { pick, uniq } from 'lodash-es'; import * as i1$1 from 'ngx-bootstrap/modal'; import { Subject, BehaviorSubject, combineLatest, of, firstValueFrom, map as map$1 } from 'rxjs'; import { map, takeUntil, shareReplay } from 'rxjs/operators'; import * as i5 from '@angular/common'; import { ApplicationType } from '@c8y/client'; import * as i4 from '@ngx-translate/core'; import * as i2$1 from '@angular/forms'; import { FormControl } from '@angular/forms'; class AppStatePipe { constructor(ecosystemService) { this.ecosystemService = ecosystemService; } transform(app, arg) { const appState = this.ecosystemService.getAppState(app); return appState[arg]; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppStatePipe, deps: [{ token: i1.EcosystemService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AppStatePipe, name: "appState" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppStatePipe, decorators: [{ type: Pipe, args: [{ name: 'appState', pure: true }] }], ctorParameters: () => [{ type: i1.EcosystemService }] }); class AppsToUpdateRemotesSelectComponent { constructor(bsModalRef, wizardModalService, ecosystemService) { this.bsModalRef = bsModalRef; this.wizardModalService = wizardModalService; this.ecosystemService = ecosystemService; this.destroy$ = new Subject(); this.filterTerm$ = new BehaviorSubject(''); this.filteredApps$ = new BehaviorSubject([]); this.appsToUpdateRemotes = []; this.result = new Promise((resolve, reject) => { this._update = resolve; this._cancel = reject; }); } ngOnInit() { this.filteredApps$ = combineLatest([of(this.apps), this.filterTerm$]).pipe(map(([apps, filterTerm]) => filterTerm.trim().length === 0 ? apps : apps.filter((application) => this.ecosystemService.filterContainString(application.name, filterTerm)))); this.textConfig = this.updateType === 'install' ? { header: gettext('Select applications to install the plugin to'), applyButton: gettext('Install') } : { header: gettext('Select applications to uninstall the plugin from'), applyButton: gettext('Uninstall') }; } cancel() { this.bsModalRef.hide(); this._cancel(); } setSelectedApps(selected, app) { selected ? this.appsToUpdateRemotes.push(app) : (this.appsToUpdateRemotes = this.appsToUpdateRemotes.filter(application => app.key !== application.key)); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } async duplicateApp() { const wizardConfig = { headerText: gettext('Duplicate application'), headerIcon: 'c8y-copy' }; const initialState = { wizardConfig, componentInitialState: { noBackButton: true }, id: 'duplicateApplication' }; const modalOptions = { initialState }; const modalRef = this.wizardModalService.show(modalOptions); modalRef.content.onClose.pipe(takeUntil(this.destroy$)).subscribe(async () => { this.apps = await this.getOwnedHostedApps(); this.ngOnInit(); }); } async apply() { this._update(this.appsToUpdateRemotes); this.bsModalRef.hide(); } async getOwnedHostedApps() { return (await this.ecosystemService.getWebApplications()).filter(app => this.ecosystemService.isOwner(app) && app.type !== ApplicationType.EXTERNAL); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppsToUpdateRemotesSelectComponent, deps: [{ token: i1$1.BsModalRef }, { token: i2.WizardModalService }, { token: i1.EcosystemService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: AppsToUpdateRemotesSelectComponent, selector: "c8y-apps-to-update-remotes-select", inputs: { apps: "apps", updateType: "updateType", pluginName: "pluginName", appsDisabled: "appsDisabled" }, ngImport: i0, template: "<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", dependencies: [{ kind: "component", type: i2.AppIconComponent, selector: "c8y-app-icon", inputs: ["contextPath", "name", "app"] }, { kind: "component", type: i2.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.FilterInputComponent, selector: "c8y-filter", inputs: ["icon", "filterTerm"], outputs: ["onSearch"] }, { kind: "component", type: i2.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i2.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i2.ListItemCheckboxComponent, selector: "c8y-list-item-checkbox, c8y-li-checkbox", inputs: ["selected", "indeterminate", "disabled", "displayAsSwitch"], outputs: ["onSelect"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.HumanizeAppNamePipe, name: "humanizeAppName" }, { kind: "pipe", type: AppStatePipe, name: "appState" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppsToUpdateRemotesSelectComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-apps-to-update-remotes-select', template: "<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" }] }], ctorParameters: () => [{ type: i1$1.BsModalRef }, { type: i2.WizardModalService }, { type: i1.EcosystemService }], propDecorators: { apps: [{ type: Input }], updateType: [{ type: Input }], pluginName: [{ type: Input }], appsDisabled: [{ type: Input }] } }); class PluginListItemComponent { constructor(pluginService) { this.pluginService = pluginService; this.hideSource = false; this.isItemSelected = new EventEmitter(); this.packageType = PackageType.UNKNOWN; this.PACKAGE_TYPE = PackageType; } ngOnInit() { this.packageType = this.pluginService.getPackageType(this.plugin.originApp); } onChange(event) { this.plugin.selected = !this.plugin.selected; this.isItemSelected.next(event); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PluginListItemComponent, deps: [{ token: i2.PluginsService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: PluginListItemComponent, selector: "c8y-plugin-list-item", inputs: { plugin: "plugin", selectable: "selectable", hideSource: "hideSource" }, outputs: { isItemSelected: "isItemSelected" }, ngImport: i0, template: "<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\">\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", dependencies: [{ kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i2.ListItemCheckboxComponent, selector: "c8y-list-item-checkbox, c8y-li-checkbox", inputs: ["selected", "indeterminate", "disabled", "displayAsSwitch"], outputs: ["onSelect"] }, { kind: "pipe", type: i1.TranslatePackageLabelPipe, name: "translatePackageLabel" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PluginListItemComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-plugin-list-item', template: "<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\">\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" }] }], ctorParameters: () => [{ type: i2.PluginsService }], propDecorators: { plugin: [{ type: Input }], selectable: [{ type: Input }], hideSource: [{ type: Input }], isItemSelected: [{ type: Output }] } }); class PluginListComponent { constructor(ecosystemService, bsModalService, pluginsService, alertService, translateService, gainsightService, humanizeAppNamePipe) { this.ecosystemService = ecosystemService; this.bsModalService = bsModalService; this.pluginsService = pluginsService; this.alertService = alertService; this.translateService = translateService; this.gainsightService = gainsightService; this.humanizeAppNamePipe = humanizeAppNamePipe; this.CURRENT_LOCATION = location.href; this.emptyListText = ''; this.hideSource = false; /** * Shows the install button for each plugin separately. Currently used in package-details view. */ this.installable = false; this.selectedItems = new EventEmitter(); this.remotePlugins$ = new BehaviorSubject({}); this.selectedPlugins = {}; this.updatingPluginId = { install: '', uninstall: '' }; this.appsDisabled = new Set(); } updateSelectedItems(selected, plugin) { this.selectedPlugins[plugin.id] = selected ? plugin : undefined; const onlyInstalledPlugins = Object.values(this.selectedPlugins).filter(Boolean); this.selectedItems.emit(onlyInstalledPlugins); } async installPlugin(plugin) { await this.updateAppRemotes(plugin, 'install'); } async uninstallPlugin(plugin) { await this.updateAppRemotes(plugin, 'uninstall'); } async updateAppRemotes(plugin, updateType) { this.updatingPluginId[updateType] = plugin?.id; let initialState; try { const apps = await this.getAppsForUpdate(plugin, updateType); initialState = { apps, updateType, pluginName: plugin.name, appsDisabled: this.appsDisabled }; } catch (e) { this.alertService.addServerFailure(e); this.updatingPluginId[updateType] = ''; return; } let selectedApps; try { selectedApps = await this.selectApps(initialState); if (!selectedApps) { this.updatingPluginId[updateType] = ''; return; } } catch { // unreached } if (updateType === 'install') { const isArchived = await this.ecosystemService.verifyArchived([plugin]); if (!isArchived) { this.updatingPluginId[updateType] = ''; return; } const licensesVerifiedByUser = await this.ecosystemService.verifyLicenses([plugin]); if (!licensesVerifiedByUser) { this.updatingPluginId[updateType] = ''; return; } } for (const app of selectedApps) { try { if (updateType === 'install') { const versionIsCompatible = await this.ecosystemService.verifyPluginVersionsCompatibility([plugin], app); if (!versionIsCompatible) { continue; } } await this.handleRemotesUpdate(app, plugin, updateType); const humanizedAppName = await firstValueFrom(this.humanizeAppNamePipe.transform(app)); const successText = updateType === 'install' ? this.translateService.instant(gettext('Plugin installed to application "{{ appName }}".'), { appName: humanizedAppName }) : this.translateService.instant(gettext('Plugin uninstalled from application "{{ appName }}".'), { appName: humanizedAppName }); this.alertService.success(successText); this.onUpdateEventHandleGS(plugin, app, updateType); } catch (error) { this.onUpdateEventHandleGS(plugin, app, updateType, error); } } this.updatingPluginId[updateType] = ''; } onUpdateEventHandleGS(plugin, app, updateType, error) { const pluginCustomEventInfo = pick(plugin, [ 'name', 'contextPath', 'module', 'version', 'type', 'id' ]); const gsEventResult = updateType === 'install' ? PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_INSTALLED : PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_REMOVED; const eventData = { component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.PLUGIN_LIST, result: error || gsEventResult, url: this.CURRENT_LOCATION, ...pluginCustomEventInfo, targetApplicationName: app.name, targetApplicationContextPath: app.contextPath, ...(error && { error }) }; this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.PACKAGE_PLUGINS, eventData); } async getAppsForUpdate(plugin, updateType) { let apps = (await this.ecosystemService.getWebApplications()).filter(app => this.ecosystemService.isOwner(app) && app.type !== ApplicationType.EXTERNAL); if (updateType === 'install') { this.appsDisabled.clear(); for (const app of apps) { if (this.isPluginInstalledInApp(plugin, app)) { this.appsDisabled.add(app.id); } } } if (updateType === 'uninstall') { const installedApps = []; for (const app of apps) { if (this.isPluginInstalledInApp(plugin, app)) { installedApps.push(app); } } apps = installedApps; } return apps; } isPluginInstalledInApp(plugin, app) { const appRemotes = this.pluginsService.getMFRemotes(app) || {}; for (const [remoteName, modules] of Object.entries(appRemotes)) { const pluginFromThisPackageIsInstalled = this.getPluginContextPathWithoutVersion(remoteName) === plugin.contextPath; const specificPluginModuleIsInstalled = modules.some(module => module === plugin.module); if (pluginFromThisPackageIsInstalled && specificPluginModuleIsInstalled) { return true; } } return false; } getPluginContextPathWithoutVersion(remote) { return remote.split('@')[0]; } async handleRemotesUpdate(application, plugin, updateType) { try { // When remotes object is not set in the configuration object of an application. // Fallback to setInitialRemotes is triggered. const { remotes, excludedRemotes } = await (updateType === 'install' ? this.pluginsService.addRemotes(application, plugin) : this.pluginsService.removeRemotes(application, this.getAllPluginsToRemove(plugin))); if (!application.config) { application.config = {}; } application.config.remotes = remotes; application.config.excludedRemotes = excludedRemotes; const actualRemotes = this.pluginsService.getMFRemotes(application); return this.emitRemotes(actualRemotes); } catch (er) { if (er) { this.alertService.addServerFailure(er); } throw er; } } getAllPluginsToRemove(plugin) { return this.package.applicationVersions.map(av => ({ id: `${plugin.contextPath}@${av.version}/${plugin.module}`, idLatest: `${plugin.contextPath}/${plugin.module}`, module: plugin.module, path: plugin.path })); } emitRemotes(remotes) { this.remotePlugins$.next(remotes); return { ...this.remotePlugins$.value }; } async selectApps(initialState) { try { return await this.bsModalService.show(AppsToUpdateRemotesSelectComponent, { class: 'modal-sm', ariaDescribedby: 'modal-body', ariaLabelledBy: 'modal-title', initialState, ignoreBackdropClick: true, keyboard: false }).content.result; } catch (er) { return; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PluginListComponent, deps: [{ token: i1.EcosystemService }, { token: i1$1.BsModalService }, { token: i2.PluginsService }, { token: i2.AlertService }, { token: i4.TranslateService }, { token: i2.GainsightService }, { token: i2.HumanizeAppNamePipe }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: PluginListComponent, selector: "c8y-plugin-list", inputs: { plugins$: "plugins$", emptyListText: "emptyListText", selectable: "selectable", hideSource: "hideSource", installable: "installable", package: "package" }, outputs: { selectedItems: "selectedItems" }, ngImport: i0, template: "<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 [ngClass]=\"{ disabled: plugin.installed }\" class=\"bg-inherit\">\n <c8y-plugin-list-item\n (isItemSelected)=\"updateSelectedItems($event, plugin)\"\n [plugin]=\"plugin\"\n [selectable]=\"selectable\"\n [hideSource]=\"hideSource\"\n class=\"d-flex\"\n ></c8y-plugin-list-item>\n <div class=\"p-l-40 m-t-4\">\n <button\n *ngIf=\"installable\"\n (click)=\"uninstallPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.uninstall }\"\n [disabled]=\"updatingPluginId.uninstall && plugin.id !== updatingPluginId.uninstall\"\n class=\"btn btn-danger btn-sm m-l-4\"\n title=\"{{ 'Uninstall plugin' | translate }}\"\n data-cy=\"plugin-list--uninstall-plugin-button\"\n translate\n >\n Uninstall plugin\n </button>\n <button\n *ngIf=\"installable\"\n (click)=\"installPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.install }\"\n [disabled]=\"updatingPluginId.install && plugin.id !== updatingPluginId.install\"\n class=\"btn btn-default btn-sm m-l-8\"\n title=\"{{ 'Install plugin' | translate }}\"\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 <div class=\"c8y-empty-state text-left\" *ngIf=\"emptyListText\">\n <h1 c8yIcon=\"plugin\"></h1>\n <p>\n {{ emptyListText | translate }}\n </p>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: PluginListItemComponent, selector: "c8y-plugin-list-item", inputs: ["plugin", "selectable", "hideSource"], outputs: ["isItemSelected"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PluginListComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-plugin-list', template: "<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 [ngClass]=\"{ disabled: plugin.installed }\" class=\"bg-inherit\">\n <c8y-plugin-list-item\n (isItemSelected)=\"updateSelectedItems($event, plugin)\"\n [plugin]=\"plugin\"\n [selectable]=\"selectable\"\n [hideSource]=\"hideSource\"\n class=\"d-flex\"\n ></c8y-plugin-list-item>\n <div class=\"p-l-40 m-t-4\">\n <button\n *ngIf=\"installable\"\n (click)=\"uninstallPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.uninstall }\"\n [disabled]=\"updatingPluginId.uninstall && plugin.id !== updatingPluginId.uninstall\"\n class=\"btn btn-danger btn-sm m-l-4\"\n title=\"{{ 'Uninstall plugin' | translate }}\"\n data-cy=\"plugin-list--uninstall-plugin-button\"\n translate\n >\n Uninstall plugin\n </button>\n <button\n *ngIf=\"installable\"\n (click)=\"installPlugin(plugin)\"\n [ngClass]=\"{ 'btn-pending': plugin.id === updatingPluginId.install }\"\n [disabled]=\"updatingPluginId.install && plugin.id !== updatingPluginId.install\"\n class=\"btn btn-default btn-sm m-l-8\"\n title=\"{{ 'Install plugin' | translate }}\"\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 <div class=\"c8y-empty-state text-left\" *ngIf=\"emptyListText\">\n <h1 c8yIcon=\"plugin\"></h1>\n <p>\n {{ emptyListText | translate }}\n </p>\n </div>\n</ng-template>\n" }] }], ctorParameters: () => [{ type: i1.EcosystemService }, { type: i1$1.BsModalService }, { type: i2.PluginsService }, { type: i2.AlertService }, { type: i4.TranslateService }, { type: i2.GainsightService }, { type: i2.HumanizeAppNamePipe }], propDecorators: { plugins$: [{ type: Input }], emptyListText: [{ type: Input }], selectable: [{ type: Input }], hideSource: [{ type: Input }], installable: [{ type: Input }], package: [{ type: Input }], selectedItems: [{ type: Output }] } }); class OnlyLatestFilterComponent { constructor(filterComponent) { this.filterComponent = filterComponent; this.filterLabel = gettext('Only latest versions'); this.onlyLatestFormControl = new FormControl(true); this.filterComponent.formGroup.controls.custom.addControl(this.filterLabel, this.onlyLatestFormControl); filterComponent.customFilters.set(this.filterLabel, (plugin, enabled) => { return !enabled || !!plugin.tags?.includes('latest'); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OnlyLatestFilterComponent, deps: [{ token: i1.ListFiltersComponent }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: OnlyLatestFilterComponent, selector: "c8y-only-latest-filter", ngImport: i0, template: "<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", dependencies: [{ kind: "directive", type: i2$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OnlyLatestFilterComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-only-latest-filter', template: "<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" }] }], ctorParameters: () => [{ type: i1.ListFiltersComponent }] }); class InstallPluginComponent { constructor(bsModalRef, ecosystemService) { this.bsModalRef = bsModalRef; this.ecosystemService = ecosystemService; this.filteredPlugins$ = new BehaviorSubject([]); this.selectedPlugins = []; this.packageTypes = defaultPackageTypes; this.result = new Promise((resolve, reject) => { this._install = resolve; this._cancel = reject; }); this.onlyLatestPluginVersion = true; } setFilterPipe(filterPipe) { this.filteredPlugins$ = this.plugins$.pipe(map$1(plugins => plugins.map(plugin => { plugin.filterProps = this.ecosystemService.getAppFilterProps(plugin.originApp); return plugin; })), src => filterPipe(src)); } cancel() { this.bsModalRef.hide(); this._cancel(); } install() { this._install(this.selectedPlugins); this.bsModalRef.hide(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: InstallPluginComponent, deps: [{ token: i1$1.BsModalRef }, { token: i1.EcosystemService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: InstallPluginComponent, selector: "c8y-install-plugin", inputs: { plugins$: "plugins$" }, ngImport: i0, template: "<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'plugin'\"></i>\n <div\n class=\"modal-title h4\"\n id=\"modal-title\"\n translate\n >\n Available plugins\n </div>\n </div>\n <div class=\"p-t-8 p-16 text-center separator-bottom flex-no-shrink\">\n <p\n class=\"text-medium m-b-8\"\n translate\n >\n Select the compatible plugins to install\n </p>\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\n class=\"modal-inner-scroll\"\n id=\"modal-body\"\n >\n <c8y-plugin-list\n class=\"m-t-16\"\n (selectedItems)=\"selectedPlugins = $event\"\n [emptyListText]=\"'No plugins available' | translate\"\n [plugins$]=\"filteredPlugins$\"\n [selectable]=\"true\"\n ></c8y-plugin-list>\n </div>\n\n <div class=\"modal-footer\">\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</div>\n", dependencies: [{ kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i1.ListFiltersComponent, selector: "c8y-list-filters", inputs: ["packageTypes", "packageAvailabilities", "packageContents"], outputs: ["filterPipeChange"] }, { kind: "component", type: i1.ArchivedFilterComponent, selector: "c8y-archived-filter" }, { kind: "component", type: PluginListComponent, selector: "c8y-plugin-list", inputs: ["plugins$", "emptyListText", "selectable", "hideSource", "installable", "package"], outputs: ["selectedItems"] }, { kind: "component", type: OnlyLatestFilterComponent, selector: "c8y-only-latest-filter" }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: InstallPluginComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-install-plugin', template: "<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'plugin'\"></i>\n <div\n class=\"modal-title h4\"\n id=\"modal-title\"\n translate\n >\n Available plugins\n </div>\n </div>\n <div class=\"p-t-8 p-16 text-center separator-bottom flex-no-shrink\">\n <p\n class=\"text-medium m-b-8\"\n translate\n >\n Select the compatible plugins to install\n </p>\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\n class=\"modal-inner-scroll\"\n id=\"modal-body\"\n >\n <c8y-plugin-list\n class=\"m-t-16\"\n (selectedItems)=\"selectedPlugins = $event\"\n [emptyListText]=\"'No plugins available' | translate\"\n [plugins$]=\"filteredPlugins$\"\n [selectable]=\"true\"\n ></c8y-plugin-list>\n </div>\n\n <div class=\"modal-footer\">\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</div>\n" }] }], ctorParameters: () => [{ type: i1$1.BsModalRef }, { type: i1.EcosystemService }], propDecorators: { plugins$: [{ type: Input }] } }); class LabelCellRendererComponent { constructor(context) { this.context = context; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LabelCellRendererComponent, deps: [{ token: i2.CellRendererContext }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: LabelCellRendererComponent, selector: "c8y-label-cell-renderer", ngImport: i0, template: "<span class=\"label label-info\">{{ context.value }}</span>\n" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LabelCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-label-cell-renderer', template: "<span class=\"label label-info\">{{ context.value }}</span>\n" }] }], ctorParameters: () => [{ type: i2.CellRendererContext }] }); class OrphanedStatusCellRendererComponent { constructor(context) { this.context = context; this.label = this.getLabel(context.value); } getLabel(statusValue) { switch (statusValue) { case ApplicationPluginStatus.OUTDATED: return { value: statusValue, text: gettext('OUTDATED`plugin status`'), class: 'label-warning' }; case ApplicationPluginStatus.ORPHANED: return { value: statusValue, text: gettext('ORPHANED`plugin status`'), class: 'label-danger' }; case ApplicationPluginStatus.REVOKED: return { value: statusValue, text: gettext('REVOKED`plugin status`'), class: 'label-danger' }; case ApplicationPluginStatus.LATEST: return { value: statusValue, text: gettext('LATEST`plugin status`'), class: 'label-success' }; case ApplicationPluginStatus.AUTO: return { value: statusValue, text: gettext('AUTO`plugin status`'), class: 'label-success' }; default: return null; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OrphanedStatusCellRendererComponent, deps: [{ token: i2.CellRendererContext }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: OrphanedStatusCellRendererComponent, selector: "c8y-orphaned-status-cell-renderer", ngImport: i0, template: "<span\n *ngIf=\"label\"\n class=\"label\"\n [ngClass]=\"label.class\"\n [title]=\"label.text | translate\"\n>\n {{ label.text | translate }}\n</span>\n", dependencies: [{ kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OrphanedStatusCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-orphaned-status-cell-renderer', template: "<span\n *ngIf=\"label\"\n class=\"label\"\n [ngClass]=\"label.class\"\n [title]=\"label.text | translate\"\n>\n {{ label.text | translate }}\n</span>\n" }] }], ctorParameters: () => [{ type: i2.CellRendererContext }] }); class UpdatePluginOfAppComponent { constructor(bsModalRef, pluginsService, alert, ecosystemService, gainsightService) { this.bsModalRef = bsModalRef; this.pluginsService = pluginsService; this.alert = alert; this.ecosystemService = ecosystemService; this.gainsightService = gainsightService; this.CURRENT_LOCATION = location.href; this.result = new Promise((resolve, reject) => { this._install = resolve; this._cancel = reject; }); this.updateAll = true; } async update() { const remotes = this.pluginsService.getMFRemotes(this.app); const oldRemotePath = `${this.plugin.contextPath}@${this.plugin.version}`; const newRemotePath = `${this.plugin.contextPath}@${this.applicationVersion.version}`; let oldRemoteModules = [...(remotes[oldRemotePath] || [])]; if (!oldRemoteModules.length) { this.alert.warning(gettext('Could not change the version of plugin.')); this.cancel(); return; } const isArchived = await this.ecosystemService.verifyArchived([this.plugin]); if (!isArchived) { this.alert.warning(gettext('Plugin update aborted by user.')); this.cancel(); return; } const result = await this.ecosystemService.verifyLicenses([this.plugin]); if (!result) { this.alert.warning(gettext('Plugin update aborted by user.')); this.cancel(); return; } let remoteModulesOfNewVersion = [...(remotes[newRemotePath] || [])]; let olderVersions = {}; if (this.updateAll) { olderVersions = Object.keys(remotes) .filter(key => key.startsWith(`${this.plugin.contextPath}@`)) .reduceRight((prev, curr) => { prev[curr] = undefined; return prev; }, {}); oldRemoteModules = Object.keys(olderVersions) .map(version => remotes[version]) .reduceRight((prev, curr) => { prev.push(...curr); return prev; }, []); remoteModulesOfNewVersion.push(...oldRemoteModules); } else { remoteModulesOfNewVersion.push(this.plugin.module); olderVersions[oldRemotePath] = oldRemoteModules.filter(module => module !== this.plugin.module); if (!olderVersions[oldRemotePath].length) { olderVersions[oldRemotePath] = undefined; } } remoteModulesOfNewVersion = uniq(remoteModulesOfNewVersion); const newRemotes = { ...remotes, ...olderVersions, [newRemotePath]: remoteModulesOfNewVersion }; try { await this.pluginsService.updateRemotesInAppConfig(this.app, newRemotes); this.alert.success(gettext(`Switched the version of plugin.`)); this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.INSTALLED_PLUGINS, { component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.UPDATE_PLUGIN_OF_APP, action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.CHANGE_PLUGIN_VERSION, result: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.PLUGIN_VERSION_CHANGED, url: this.CURRENT_LO