@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
546 lines (540 loc) • 369 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, Input, Pipe, EventEmitter, Output, ViewChild, Injectable, NgModule } from '@angular/core';
import * as i2 from '@c8y/ngx-components';
import { gettext, Status, PackageType, PluginsService, PluginsExportScopes, Permissions, ViewContext, ApplicationPluginStatus, DataGridComponent, CoreModule, hookRoute, C8yStepper, FormsModule, hookTab, hookNavigator, hookWizard } from '@c8y/ngx-components';
import * as i1 from '@c8y/ngx-components/ecosystem/shared';
import { PRODUCT_EXPERIENCE_ECOSYSTEM, packageProperties, defaultPackageAvailabilities, EcosystemWizards, ListFiltersComponent, ApplicationPropertiesFormComponent, defaultPackageTypes, SharedEcosystemModule, ERROR_TYPE, APP_STATE, PACKAGE_TYPE_LABELS, defaultPackageContents } from '@c8y/ngx-components/ecosystem/shared';
import * as i3 from '@angular/common';
import * as i2$1 from '@angular/forms';
import { Validators, FormControl, ReactiveFormsModule } from '@angular/forms';
import * as i1$2 from '@angular/router';
import { RouterModule } from '@angular/router';
import * as i4 from '@c8y/client';
import { ApplicationType, Isolation, BillingMode } from '@c8y/client';
import * as i4$1 from '@ngx-translate/core';
import * as i1$1 from 'ngx-bootstrap/modal';
import { isEmpty } from 'lodash';
import { Subject, BehaviorSubject, combineLatest, of, firstValueFrom, from, map as map$1 } from 'rxjs';
import { map, takeUntil, tap, switchMap, shareReplay } from 'rxjs/operators';
import { pick, uniq } from 'lodash-es';
import * as i9 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i10 from '@c8y/ngx-components/icon-selector';
import { IconSelectorModule } from '@c8y/ngx-components/icon-selector';
import { A11yModule } from '@angular/cdk/a11y';
import { ArchivedConfirmModule } from '@c8y/ngx-components/ecosystem/archived-confirm';
import { LicenseConfirmModule } from '@c8y/ngx-components/ecosystem/license-confirm';
import * as i6 from 'ngx-bootstrap/dropdown';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { PopoverModule } from 'ngx-bootstrap/popover';
class ActivityLogComponent {
constructor(ecosystemService, alertService) {
this.ecosystemService = ecosystemService;
this.alertService = alertService;
this.hasAdminPermissions = false;
this.archives = [];
this.canReactivate = false;
}
get uploadProgress() {
return this.ecosystemService.progress;
}
async ngOnInit() {
this.canReactivate = this.showReactivate();
this.refresh();
}
isActive(archive) {
return this.application.activeVersionId === archive.id;
}
toActivate(archive) {
return this.toActivateVersionId === archive.id;
}
checkIfLast(archive) {
return archive.id === this.last.id;
}
showReactivate() {
return this.ecosystemService.isApplication(this.application);
}
async setActive(archive) {
const id = archive.id || archive;
this.toActivateVersionId = id;
this.isLoading = true;
try {
this.application = (await this.ecosystemService.setAppActiveVersion(this.application, id)).data;
}
catch (ex) {
this.alertService.addServerFailure(ex);
}
this.isLoading = false;
this.refresh();
}
async deleteArchive(archive) {
await this.ecosystemService.deleteArchive(archive, this.application);
this.refresh();
}
async downloadArchive(archive) {
await this.ecosystemService.downloadArchive(this.application, archive);
}
async reactivateArchive() {
await this.ecosystemService.reactivateArchive(this.application);
}
async onRefresh() {
await this.refresh();
}
async refresh() {
this.isLoading = true;
this.archives = await this.ecosystemService.listArchives(this.application.id);
if (this.application.manifest?.package === 'blueprint') {
// filter out entries without description because using them as active may break application's
// manifest (changing isPackage property of deployed app to 'true')
this.archives = this.archives.filter((archive) => !!archive.description);
}
this.archives.sort((a, b) => {
return new Date(b.created) - new Date(a.created);
});
this.last = this.archives[this.archives.length - 1];
this.isLoading = false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ActivityLogComponent, deps: [{ token: i1.EcosystemService }, { token: i2.AlertService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: ActivityLogComponent, selector: "c8y-activity-log", inputs: { application: "application", hasAdminPermissions: "hasAdminPermissions" }, ngImport: i0, template: "<div class=\"inner-scroll bg-level-1 flex-grow inner-scroll--md overflow-visible-sm overflow-visible-xs\">\n <div class=\"card-block overflow-visible\">\n <c8y-list-group>\n <c8y-li-timeline *ngFor=\"let archive of archives\" [ngClass]=\"{ active: isActive(archive) }\">\n {{ archive.created | date: 'd MMM YYYY' }}\n {{ archive.created | date: 'shortTime' }}\n <c8y-li>\n <c8y-li-icon\n [icon]=\"checkIfLast(archive) ? 'flag-checkered' : 'file-zip-o'\"\n ></c8y-li-icon>\n <c8y-li-body>\n <div class=\"d-flex a-i-start\">\n <div style=\"min-width: 0; flex: 1\">\n <span class=\"text-truncate-wrap\" title=\" {{ archive.description || archive.name }}\">\n {{ archive.description || archive.name }}\n </span>\n <small *ngIf=\"archive.description\" class=\"text-muted\">{{\n archive.description\n }}</small>\n </div>\n <i\n *ngIf=\"isLoading && toActivate(archive)\"\n [c8yIcon]=\"'circle-o-notch'\"\n class=\"icon-spin\"\n title=\"{{ 'Activating' | translate }}\"\n ></i>\n\n <span *ngIf=\"isActive(archive)\" class=\"label label-primary m-l-auto m-t-4\">{{\n 'Active' | translate\n }}</span>\n </div>\n </c8y-li-body>\n <c8y-li-action\n (click)=\"setActive(archive)\"\n *ngIf=\"hasAdminPermissions && !isLoading && !isActive(archive)\"\n icon=\"check-square-o\"\n >\n {{ 'Set as active`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action (click)=\"downloadArchive(archive)\" icon=\"download\">\n {{ 'Download`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action\n (click)=\"deleteArchive(archive)\"\n *ngIf=\"\n hasAdminPermissions &&\n archives.length > 1 &&\n !checkIfLast(archive) &&\n !isActive(archive)\n \"\n icon=\"delete\"\n >\n {{ 'Delete`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action\n (click)=\"reactivateArchive()\"\n *ngIf=\"hasAdminPermissions && canReactivate && isActive(archive)\"\n icon=\"undo\"\n >\n {{ 'Reactivate archive' | translate }}\n </c8y-li-action>\n </c8y-li>\n </c8y-li-timeline>\n </c8y-list-group>\n </div>\n</div>\n<div class=\"card-footer\" *ngIf=\"!isLoading && hasAdminPermissions\">\n <c8y-form-group class=\"m-auto\">\n <c8y-upload-archive [(application)]=\"application\" (refresh)=\"onRefresh()\"></c8y-upload-archive>\n </c8y-form-group>\n</div>\n", dependencies: [{ kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { 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.ListItemBodyComponent, selector: "c8y-list-item-body, c8y-li-body", inputs: ["body"] }, { kind: "component", type: i2.ListItemActionComponent, selector: "c8y-list-item-action, c8y-li-action", inputs: ["label", "icon", "disabled"], outputs: ["click"] }, { kind: "component", type: i2.ListItemTimelineComponent, selector: "c8y-list-item-timeline, c8y-li-timeline" }, { kind: "component", type: i1.UploadArchiveComponent, selector: "c8y-upload-archive", inputs: ["application", "uploadNewVersion", "preUploadCallback"], outputs: ["applicationChange", "refresh"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i3.DatePipe, name: "date" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ActivityLogComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-activity-log', template: "<div class=\"inner-scroll bg-level-1 flex-grow inner-scroll--md overflow-visible-sm overflow-visible-xs\">\n <div class=\"card-block overflow-visible\">\n <c8y-list-group>\n <c8y-li-timeline *ngFor=\"let archive of archives\" [ngClass]=\"{ active: isActive(archive) }\">\n {{ archive.created | date: 'd MMM YYYY' }}\n {{ archive.created | date: 'shortTime' }}\n <c8y-li>\n <c8y-li-icon\n [icon]=\"checkIfLast(archive) ? 'flag-checkered' : 'file-zip-o'\"\n ></c8y-li-icon>\n <c8y-li-body>\n <div class=\"d-flex a-i-start\">\n <div style=\"min-width: 0; flex: 1\">\n <span class=\"text-truncate-wrap\" title=\" {{ archive.description || archive.name }}\">\n {{ archive.description || archive.name }}\n </span>\n <small *ngIf=\"archive.description\" class=\"text-muted\">{{\n archive.description\n }}</small>\n </div>\n <i\n *ngIf=\"isLoading && toActivate(archive)\"\n [c8yIcon]=\"'circle-o-notch'\"\n class=\"icon-spin\"\n title=\"{{ 'Activating' | translate }}\"\n ></i>\n\n <span *ngIf=\"isActive(archive)\" class=\"label label-primary m-l-auto m-t-4\">{{\n 'Active' | translate\n }}</span>\n </div>\n </c8y-li-body>\n <c8y-li-action\n (click)=\"setActive(archive)\"\n *ngIf=\"hasAdminPermissions && !isLoading && !isActive(archive)\"\n icon=\"check-square-o\"\n >\n {{ 'Set as active`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action (click)=\"downloadArchive(archive)\" icon=\"download\">\n {{ 'Download`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action\n (click)=\"deleteArchive(archive)\"\n *ngIf=\"\n hasAdminPermissions &&\n archives.length > 1 &&\n !checkIfLast(archive) &&\n !isActive(archive)\n \"\n icon=\"delete\"\n >\n {{ 'Delete`archive`' | translate }}\n </c8y-li-action>\n <c8y-li-action\n (click)=\"reactivateArchive()\"\n *ngIf=\"hasAdminPermissions && canReactivate && isActive(archive)\"\n icon=\"undo\"\n >\n {{ 'Reactivate archive' | translate }}\n </c8y-li-action>\n </c8y-li>\n </c8y-li-timeline>\n </c8y-list-group>\n </div>\n</div>\n<div class=\"card-footer\" *ngIf=\"!isLoading && hasAdminPermissions\">\n <c8y-form-group class=\"m-auto\">\n <c8y-upload-archive [(application)]=\"application\" (refresh)=\"onRefresh()\"></c8y-upload-archive>\n </c8y-form-group>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1.EcosystemService }, { type: i2.AlertService }], propDecorators: { application: [{
type: Input
}], hasAdminPermissions: [{
type: Input
}] } });
class SubscriptionModalComponent {
constructor(bsModalRef, ecosystemService, tabsService, modal, applicationService, alertService, contextRouteService) {
this.bsModalRef = bsModalRef;
this.ecosystemService = ecosystemService;
this.tabsService = tabsService;
this.modal = modal;
this.applicationService = applicationService;
this.alertService = alertService;
this.contextRouteService = contextRouteService;
this.RETRY_TIMEOUT = 3000;
this.isLoading = false;
this.result = new Promise(resolve => {
this._resolve = resolve;
});
this.retryCounter = 0;
this.TABS = ['Logs', 'Status'];
}
ngOnInit() {
if (this.isSubscribed) {
this.unsubscribe();
}
else {
this.subscribe();
}
}
async subscribe() {
this.retryCounter = 0;
this.isLoading = true;
this.message = gettext('Subscribing…');
await this.ecosystemService.subscribeApp(this.application);
this.getStatusDetails('subscribe');
}
async unsubscribe() {
this.retryCounter = 0;
this.isLoading = true;
this.message = gettext('Unsubscribing…');
await this.ecosystemService.unsubscribeApp(this.application);
this.getStatusDetails('unsubscribe');
}
async getStatusDetails(action) {
this.contextRouteService.refreshContext();
const actionSuccessful = action === 'subscribe' ? await this.onSubscribe() : this.onUnsubscribe();
if (actionSuccessful) {
return this.hideSubscriptionModal();
}
if (this.retryCounter === 4) {
this.showWarningModal(action);
return this.hideSubscriptionModal();
}
this.retryCounter += 1;
setTimeout(async () => {
this.getStatusDetails(action);
}, this.RETRY_TIMEOUT);
}
async onSubscribe() {
try {
if (!this.application.activeVersionId) {
return true;
}
const res = (await this.applicationService.getStatusDetails(this.application)).data[0];
return this.shouldShowMSSpecificTabs(res);
}
catch (er) {
this.alertService.addServerFailure(er);
}
}
// Checks if the UI should show tabs with logs and status
shouldShowMSSpecificTabs(mo) {
return !isEmpty(mo.c8y_Status?.instances) && !!mo.c8y_SupportedLogs;
}
onUnsubscribe() {
return !this.tabsService.areAvailable(this.TABS);
}
hideSubscriptionModal() {
this._resolve();
this.bsModalRef.hide();
this.isLoading = false;
}
showWarningModal(action) {
const title = gettext('Warning');
const body = action === 'subscribe'
? gettext('Something went wrong, please refresh the page or resubscribe the application.')
: gettext('Something went wrong, please refresh the page or retry to unsubscribe from the application.');
this.modal.acknowledge(title, body, Status.WARNING, gettext('Close'));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SubscriptionModalComponent, deps: [{ token: i1$1.BsModalRef }, { token: i1.EcosystemService }, { token: i2.TabsService }, { token: i2.ModalService }, { token: i4.ApplicationService }, { token: i2.AlertService }, { token: i2.ContextRouteService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: SubscriptionModalComponent, selector: "c8y-subscription-modal", ngImport: i0, template: "<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i c8yIcon=\"c8y-atom\"></i>\n <h4 id=\"modal-title\">{{ message | translate }}</h4>\n </div>\n <div class=\"modal-body\" id=\"modal-body\" *ngIf=\"isLoading\">\n <div class=\"p-16 text-center\">\n <c8y-loading></c8y-loading>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SubscriptionModalComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-subscription-modal', template: "<div class=\"viewport-modal\">\n <div class=\"modal-header dialog-header\">\n <i c8yIcon=\"c8y-atom\"></i>\n <h4 id=\"modal-title\">{{ message | translate }}</h4>\n </div>\n <div class=\"modal-body\" id=\"modal-body\" *ngIf=\"isLoading\">\n <div class=\"p-16 text-center\">\n <c8y-loading></c8y-loading>\n </div>\n </div>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1$1.BsModalRef }, { type: i1.EcosystemService }, { type: i2.TabsService }, { type: i2.ModalService }, { type: i4.ApplicationService }, { type: i2.AlertService }, { type: i2.ContextRouteService }] });
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: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.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: i3.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: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.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$1.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: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.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: i3.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)\