UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines 283 kB
{"version":3,"file":"c8y-ngx-components-ecosystem.mjs","sources":["../../ecosystem/activity-log/activity-log.component.ts","../../ecosystem/activity-log/activity-log.component.html","../../ecosystem/application-properties/subscription-modal/subscription-modal.component.ts","../../ecosystem/application-properties/subscription-modal/subscription-modal.component.html","../../ecosystem/application-properties/update-application-modal/update-application-modal.component.ts","../../ecosystem/application-properties/update-application-modal/update-application-modal.component.html","../../ecosystem/application-properties/application-properties.component.ts","../../ecosystem/application-properties/application-properties.component.html","../../ecosystem/application-properties/application-properties.guard.ts","../../ecosystem/applications/add-external-applicaiton/add-external-application.component.ts","../../ecosystem/applications/add-external-applicaiton/add-external-application.component.html","../../ecosystem/applications/add-web-application/add-web-application.component.ts","../../ecosystem/applications/application-list/application-list.component.ts","../../ecosystem/applications/application-list/application-list.component.html","../../ecosystem/applications/install-from-package/install-from-package.component.ts","../../ecosystem/applications/install-from-package/install-from-package.component.html","../../ecosystem/ecosystem-tabs.factory.ts","../../ecosystem/features/feature-list.component.ts","../../ecosystem/features/feature-list.component.html","../../ecosystem/microservices/add-microservice.component.ts","../../ecosystem/microservices/microservice-list.component.ts","../../ecosystem/microservices/microservice-list.component.html","../../ecosystem/packages/add-package.component.ts","../../ecosystem/packages/deploy-application/deploy-application.component.ts","../../ecosystem/packages/deploy-application/deploy-application.component.html","../../ecosystem/packages/package-details/package-details.component.ts","../../ecosystem/packages/package-details/package-details.component.html","../../ecosystem/packages/package-list/packages-list.component.ts","../../ecosystem/packages/package-list/packages-list.component.html","../../ecosystem/packages/package-versions.guard.ts","../../ecosystem/packages/package-versions/package-contents/contents-apps/contents-apps.component.ts","../../ecosystem/packages/package-versions/package-contents/contents-apps/contents-apps.component.html","../../ecosystem/packages/package-versions/package-contents/contents-plugins/contents-plugins.component.ts","../../ecosystem/packages/package-versions/package-contents/contents-plugins/contents-plugins.component.html","../../ecosystem/packages/package-versions/package-contents/packages-contents.component.ts","../../ecosystem/packages/package-versions/package-contents/packages-contents.component.html","../../ecosystem/packages/package-versions/package-versions-list/package-versions-list.component.ts","../../ecosystem/packages/package-versions/package-versions-list/package-versions-list.component.html","../../ecosystem/packages/package-versions/packages-versions.component.ts","../../ecosystem/packages/package-versions/packages-versions.component.html","../../ecosystem/packages/package.guard.ts","../../ecosystem/packages/package-changelog-tab/package-changelog-tab.component.ts","../../ecosystem/packages/package-changelog-tab/package-changelog-tab.component.html","../../ecosystem/packages/package-changelog.guard.ts","../../ecosystem/ecosystem.module.ts","../../ecosystem/c8y-ngx-components-ecosystem.ts"],"sourcesContent":["import { Component, Input, OnInit } from '@angular/core';\nimport { IApplication, IApplicationBinary } from '@c8y/client';\nimport {\n AlertService,\n DatePipe,\n ListGroupComponent,\n ListItemTimelineComponent,\n ListItemComponent,\n ListItemIconComponent,\n ListItemBodyComponent,\n IconDirective,\n ListItemActionComponent,\n FormGroupComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { BehaviorSubject } from 'rxjs';\nimport { EcosystemService, UploadArchiveComponent } from '@c8y/ngx-components/ecosystem/shared';\nimport { NgFor, NgClass, NgIf } from '@angular/common';\n\n@Component({\n selector: 'c8y-activity-log',\n templateUrl: './activity-log.component.html',\n imports: [\n ListGroupComponent,\n NgFor,\n ListItemTimelineComponent,\n NgClass,\n ListItemComponent,\n ListItemIconComponent,\n ListItemBodyComponent,\n NgIf,\n IconDirective,\n ListItemActionComponent,\n FormGroupComponent,\n UploadArchiveComponent,\n C8yTranslatePipe,\n DatePipe\n ]\n})\nexport class ActivityLogComponent implements OnInit {\n @Input() application: IApplication;\n @Input() hasAdminPermissions = false;\n archives: IApplicationBinary[] = [];\n toActivateVersionId: string;\n last: IApplicationBinary;\n isLoading: boolean;\n canReactivate = false;\n\n constructor(\n private ecosystemService: EcosystemService,\n private alertService: AlertService\n ) {}\n\n get uploadProgress(): BehaviorSubject<number> {\n return this.ecosystemService.progress;\n }\n\n async ngOnInit() {\n this.canReactivate = this.showReactivate();\n this.refresh();\n }\n\n isActive(archive: IApplicationBinary): boolean {\n return this.application.activeVersionId === archive.id;\n }\n\n toActivate(archive: IApplicationBinary): boolean {\n return this.toActivateVersionId === archive.id;\n }\n\n checkIfLast(archive: IApplicationBinary): boolean {\n return archive.id === this.last.id;\n }\n\n showReactivate(): boolean {\n return this.ecosystemService.isApplication(this.application);\n }\n\n async setActive(archive): Promise<void> {\n const id = archive.id || archive;\n this.toActivateVersionId = id;\n this.isLoading = true;\n try {\n this.application = (\n await this.ecosystemService.setAppActiveVersion(this.application, id)\n ).data;\n } catch (ex) {\n this.alertService.addServerFailure(ex);\n }\n this.isLoading = false;\n this.refresh();\n }\n\n async deleteArchive(archive: IApplicationBinary): Promise<void> {\n await this.ecosystemService.deleteArchive(archive, this.application);\n this.refresh();\n }\n\n async downloadArchive(archive: IApplicationBinary): Promise<void> {\n await this.ecosystemService.downloadArchive(this.application, archive);\n }\n\n async reactivateArchive(): Promise<void> {\n await this.ecosystemService.reactivateArchive(this.application);\n }\n\n async onRefresh() {\n await this.refresh();\n }\n\n private async refresh(): Promise<void> {\n this.isLoading = true;\n this.archives = await this.ecosystemService.listArchives(this.application.id);\n if (this.application.manifest?.package === 'blueprint') {\n // filter out entries without description because using them as active may break application's\n // manifest (changing isPackage property of deployed app to 'true')\n this.archives = this.archives.filter((archive: IApplicationBinary) => !!archive.description);\n }\n this.archives.sort((a, b) => {\n return (new Date(b.created) as any) - (new Date(a.created) as any);\n });\n this.last = this.archives[this.archives.length - 1];\n this.isLoading = false;\n }\n}\n","<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 | c8yDate: 'd MMM YYYY' }}\n {{ archive.created | c8yDate: '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","import { Component, OnInit } from '@angular/core';\nimport { ApplicationService, IApplication, IApplicationManagedObject } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AlertService,\n ContextRouteService,\n ModalService,\n Status,\n TabsService,\n IconDirective,\n LoadingComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { isEmpty } from 'lodash';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { EcosystemService } from '@c8y/ngx-components/ecosystem/shared';\nimport { NgIf } from '@angular/common';\n\n@Component({\n selector: 'c8y-subscription-modal',\n templateUrl: './subscription-modal.component.html',\n imports: [IconDirective, NgIf, LoadingComponent, C8yTranslatePipe]\n})\nexport class SubscriptionModalComponent implements OnInit {\n readonly RETRY_TIMEOUT = 3000;\n application: IApplication;\n message: string;\n isLoading = false;\n result: Promise<void> = new Promise(resolve => {\n this._resolve = resolve;\n });\n isSubscribed: boolean;\n retryCounter = 0;\n\n private readonly TABS: string[] = ['Logs', 'Status'];\n private _resolve: (value: void | PromiseLike<void>) => void;\n\n constructor(\n private bsModalRef: BsModalRef,\n private ecosystemService: EcosystemService,\n private tabsService: TabsService,\n private modal: ModalService,\n private applicationService: ApplicationService,\n private alertService: AlertService,\n private contextRouteService: ContextRouteService\n ) {}\n\n ngOnInit() {\n if (this.isSubscribed) {\n this.unsubscribe();\n } else {\n this.subscribe();\n }\n }\n\n async subscribe(): Promise<void> {\n this.retryCounter = 0;\n this.isLoading = true;\n this.message = gettext('Subscribing…');\n await this.ecosystemService.subscribeApp(this.application);\n this.getStatusDetails('subscribe');\n }\n\n async unsubscribe(): Promise<void> {\n this.retryCounter = 0;\n this.isLoading = true;\n this.message = gettext('Unsubscribing…');\n await this.ecosystemService.unsubscribeApp(this.application);\n this.getStatusDetails('unsubscribe');\n }\n\n private async getStatusDetails(action: 'subscribe' | 'unsubscribe') {\n this.contextRouteService.refreshContext();\n const actionSuccessful =\n action === 'subscribe' ? await this.onSubscribe() : this.onUnsubscribe();\n if (actionSuccessful) {\n return this.hideSubscriptionModal();\n }\n if (this.retryCounter === 4) {\n this.showWarningModal(action);\n return this.hideSubscriptionModal();\n }\n this.retryCounter += 1;\n setTimeout(async () => {\n this.getStatusDetails(action);\n }, this.RETRY_TIMEOUT);\n }\n\n private async onSubscribe(): Promise<boolean> {\n try {\n if (!this.application.activeVersionId) {\n return true;\n }\n const res: IApplicationManagedObject = (\n await this.applicationService.getStatusDetails(this.application)\n ).data[0];\n return this.shouldShowMSSpecificTabs(res);\n } catch (er) {\n this.alertService.addServerFailure(er);\n }\n }\n\n // Checks if the UI should show tabs with logs and status\n private shouldShowMSSpecificTabs(mo: IApplicationManagedObject): boolean {\n return !isEmpty(mo.c8y_Status?.instances) && !!mo.c8y_SupportedLogs;\n }\n\n private onUnsubscribe(): boolean {\n return !this.tabsService.areAvailable(this.TABS);\n }\n\n private hideSubscriptionModal(): void {\n this._resolve();\n this.bsModalRef.hide();\n this.isLoading = false;\n }\n\n private showWarningModal(action: string): void {\n const title = gettext('Warning');\n const body =\n action === 'subscribe'\n ? gettext('Something went wrong, please refresh the page or resubscribe the application.')\n : gettext(\n 'Something went wrong, please refresh the page or retry to unsubscribe from the application.'\n );\n this.modal.acknowledge(title, body, Status.WARNING, gettext('Close'));\n }\n}\n","<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","import { Component, OnInit, ViewChild } from '@angular/core';\nimport { IApplication, InventoryService } from '@c8y/client';\nimport {\n ApplicationPlugin,\n PluginsService,\n PluginsExportScopes,\n ModalComponent,\n C8yTranslateDirective,\n ListGroupComponent,\n ListItemComponent,\n ListItemIconComponent,\n ListItemBodyComponent,\n ListItemFooterComponent,\n ListItemCollapseComponent,\n LoadingComponent,\n OperationResultComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { EcosystemService } from '@c8y/ngx-components/ecosystem/shared';\nimport { BehaviorSubject } from 'rxjs';\nimport { NgIf, AsyncPipe } from '@angular/common';\nimport { PluginListComponent } from '@c8y/ngx-components/ecosystem/application-plugins';\n\n@Component({\n selector: 'c8y-update-application-modal',\n templateUrl: './update-application-modal.component.html',\n imports: [\n ModalComponent,\n NgIf,\n C8yTranslateDirective,\n ListGroupComponent,\n ListItemComponent,\n ListItemIconComponent,\n ListItemBodyComponent,\n ListItemFooterComponent,\n ListItemCollapseComponent,\n PluginListComponent,\n LoadingComponent,\n OperationResultComponent,\n C8yTranslatePipe,\n AsyncPipe\n ]\n})\nexport class UpdateApplicationModalComponent implements OnInit {\n @ViewChild('modal', { static: false }) private modal;\n isUpdateOngoing = false;\n updateFailure = false;\n sourcePackage: IApplication;\n application: IApplication;\n result: Promise<void> = new Promise((resolve, reject) => {\n this._resolve = resolve;\n this._reject = reject;\n });\n orphanedPlugins$: BehaviorSubject<ApplicationPlugin[]> = new BehaviorSubject([]);\n newPlugins$: BehaviorSubject<ApplicationPlugin[]> = new BehaviorSubject([]);\n\n private _resolve: (value: void | PromiseLike<void>) => void;\n private _reject: (value: void | PromiseLike<void>) => void;\n\n constructor(\n private inventoryService: InventoryService,\n private ecosystemService: EcosystemService,\n private pluginsService: PluginsService\n ) {}\n\n ngOnInit(): void {\n const hasDelta = this.getRemoteDelta();\n if (!hasDelta) {\n // start update directly if no remote delta detected\n this.updateApplication();\n }\n }\n\n /**\n * Checks if there is a delta between the current application and the source package remotes.\n * @returns {boolean} true if there is a delta between the current application and the source package remotes.\n */\n getRemoteDelta() {\n const currentRemotes = this.pluginsService.getMFExports(this.application, []);\n const possibleNewRemotes = this.pluginsService.getMFExports(this.sourcePackage, []);\n const installedRemotes = this.pluginsService.getMFRemotes(this.application);\n const installedRemotesIds = PluginsService.convertInstalledRemotesToIds(installedRemotes);\n\n const allRemotesToRemove = this.getAllRemotesToRemove(\n currentRemotes,\n possibleNewRemotes,\n installedRemotesIds\n );\n\n const allRemotesToAdd = this.getAllRemotesToAdd(possibleNewRemotes, currentRemotes);\n\n this.orphanedPlugins$.next(allRemotesToRemove);\n this.newPlugins$.next(allRemotesToAdd);\n\n return allRemotesToRemove.length !== 0 || allRemotesToAdd.length !== 0;\n }\n\n close() {\n this._reject();\n this.modal._dismiss();\n }\n\n done() {\n if (!this.updateFailure) {\n this._resolve();\n this.modal._dismiss();\n return;\n }\n this._reject();\n this.modal._dismiss();\n }\n\n async updateApplication() {\n try {\n this.isUpdateOngoing = true;\n const toInstallPlugins = this.newPlugins$.value.filter(({ selected }) => selected === true);\n const toRemovePlugins = this.orphanedPlugins$.value;\n this.orphanedPlugins$.next([]);\n this.newPlugins$.next([]);\n if (toRemovePlugins.length > 0) {\n const remotes = await this.pluginsService.removeRemotes(this.application, toRemovePlugins);\n this.application.config.remotes = remotes;\n }\n if (toInstallPlugins.length > 0) {\n const remotes = await this.pluginsService.addRemotes(this.application, toInstallPlugins);\n this.application.config.remotes = remotes;\n }\n const binaryMoId = this.sourcePackage.activeVersionId;\n await this.inventoryService.detail(binaryMoId); // only trying if we can access it\n await this.ecosystemService.uploadBinaryFromOtherPackage(\n this.sourcePackage,\n this.application,\n this.sourcePackage.activeVersionId\n );\n } catch (e) {\n if (e.res?.status === 404) {\n try {\n this.updateFailure = !(await this.ecosystemService.fallbackToClone(\n this.application,\n this.sourcePackage\n ));\n } catch (ex) {\n this.updateFailure = true;\n this.ecosystemService.alertError(e);\n }\n } else {\n this.updateFailure = true;\n this.ecosystemService.alertError(e);\n }\n } finally {\n this.isUpdateOngoing = false;\n }\n }\n\n private getAllRemotesToAdd(\n possibleNewRemotes: ApplicationPlugin[],\n currentRemotes: ApplicationPlugin[]\n ) {\n return possibleNewRemotes\n .filter(possibleNewRemote => {\n const isUnchanged = !!currentRemotes.some(\n currentRemote =>\n PluginsService.createPluginId(\n this.application.contextPath,\n possibleNewRemote.module,\n '',\n true\n ) === currentRemote.idLatest\n );\n\n const isSelf =\n possibleNewRemote.scope === PluginsExportScopes.SELF ||\n possibleNewRemote.scope === PluginsExportScopes.SELF_OPTIONAL;\n\n return !isUnchanged && isSelf;\n })\n .map(newRemote => {\n newRemote.contextPath = this.application.contextPath;\n newRemote.id = `${newRemote.contextPath}/${newRemote.module}`;\n newRemote.selected = newRemote.scope === PluginsExportScopes.SELF;\n return newRemote;\n });\n }\n\n private getAllRemotesToRemove(\n currentRemotes: ApplicationPlugin[],\n possibleNewRemotes: ApplicationPlugin[],\n installedRemotesIds: string[]\n ) {\n return currentRemotes\n .filter(currentRemote => {\n const isUnchanged = !!possibleNewRemotes.some(\n newRemote =>\n PluginsService.createPluginId(\n this.application.contextPath,\n newRemote.module,\n '',\n true\n ) === currentRemote.idLatest\n );\n const isInstalled = installedRemotesIds\n ? !!installedRemotesIds.some(\n currentInstalled =>\n currentRemote.id === currentInstalled || currentRemote.idLatest === currentInstalled\n )\n : false;\n\n const isSelf =\n currentRemote.scope === PluginsExportScopes.SELF ||\n currentRemote.scope === PluginsExportScopes.SELF_OPTIONAL;\n\n return !isUnchanged && isInstalled && isSelf;\n })\n .map(toRemoveRemote => {\n toRemoveRemote.contextPath = this.application.contextPath;\n toRemoveRemote.id = `${toRemoveRemote.contextPath}/${toRemoveRemote.module}`;\n toRemoveRemote.selected = true;\n return toRemoveRemote;\n });\n }\n}\n","<c8y-modal\n [title]=\"'Update application' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n #modal\n>\n <ng-container c8y-modal-title>\n <span class=\"dlt-c8y-icon-installing-updates\"></span>\n </ng-container>\n\n <ng-container\n *ngIf=\"\n (orphanedPlugins$ | async).length > 0 || (newPlugins$ | async).length > 0;\n else updateProgress\n \"\n >\n <p\n class=\"text-center text-break-word p-24 text-14\"\n translate\n >\n Updating this blueprint will change the default plugins. Review the plugin changes before\n proceeding.\n </p>\n <c8y-list-group *ngIf=\"(orphanedPlugins$ | async).length > 0\">\n <c8y-li [collapsed]=\"true\">\n <c8y-li-icon>\n <span class=\"badge badge-danger\">{{ (orphanedPlugins$ | async).length }}</span>\n </c8y-li-icon>\n <c8y-li-body>\n <div translate>Orphaned plugins</div>\n </c8y-li-body>\n <c8y-li-footer translate>\n Some plugins are not contained in the new version of this blueprint and will therefore get\n removed.\n </c8y-li-footer>\n <c8y-li-collapse>\n <c8y-plugin-list\n class=\"m-t-16\"\n [emptyListText]=\"'No plugins available' | translate\"\n [plugins$]=\"orphanedPlugins$\"\n [selectable]=\"false\"\n [hideSource]=\"true\"\n ></c8y-plugin-list>\n </c8y-li-collapse>\n </c8y-li>\n </c8y-list-group>\n\n <c8y-list-group *ngIf=\"(newPlugins$ | async).length > 0\">\n <c8y-li [collapsed]=\"false\">\n <c8y-li-icon>\n <span class=\"badge badge-success\">{{ (newPlugins$ | async).length }}</span>\n </c8y-li-icon>\n <c8y-li-body>\n <div translate>New plugins added</div>\n </c8y-li-body>\n <c8y-li-footer translate>\n This blueprint will add new plugins. Please choose which you want to install.\n </c8y-li-footer>\n <c8y-li-collapse>\n <c8y-plugin-list\n class=\"m-t-16\"\n [emptyListText]=\"'No plugins available' | translate\"\n [plugins$]=\"newPlugins$\"\n [selectable]=\"true\"\n [hideSource]=\"false\"\n ></c8y-plugin-list>\n </c8y-li-collapse>\n </c8y-li>\n </c8y-list-group>\n </ng-container>\n\n <ng-container c8y-modal-footer-custom>\n <div\n class=\"modal-footer\"\n *ngIf=\"\n (orphanedPlugins$ | async).length > 0 || (newPlugins$ | async).length > 0;\n else updateProgressButtons\n \"\n >\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Close' | translate }}\"\n (click)=\"close()\"\n [disabled]=\"isUpdateOngoing\"\n >\n {{ 'Close' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Continue' | translate }}\"\n (click)=\"updateApplication()\"\n *ngIf=\"(orphanedPlugins$ | async).length > 0 || (newPlugins$ | async).length > 0\"\n [disabled]=\"isUpdateOngoing\"\n >\n {{ 'Continue' | translate }}\n </button>\n </div>\n </ng-container>\n\n <ng-template #updateProgressButtons>\n <ng-container c8y-modal-footer-custom>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Close' | translate }}\"\n (click)=\"done()\"\n [disabled]=\"isUpdateOngoing\"\n >\n {{ 'Close' | translate }}\n </button>\n </div>\n </ng-container>\n </ng-template>\n\n <ng-template #updateProgress>\n <c8y-loading\n class=\"text-center d-block p-t-56 p-b-56 m-t-4 m-b-4\"\n style=\"min-height: 180px\"\n layout=\"application\"\n *ngIf=\"isUpdateOngoing\"\n [message]=\"'Updating…' | translate\"\n ></c8y-loading>\n\n <c8y-operation-result\n type=\"success\"\n *ngIf=\"!isUpdateOngoing && !updateFailure\"\n text=\"{{ 'Update completed' | translate }}\"\n [size]=\"120\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n <c8y-operation-result\n type=\"error\"\n *ngIf=\"!isUpdateOngoing && updateFailure\"\n text=\"{{ 'Failed to update application.' | translate }}\"\n [size]=\"120\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n </ng-template>\n</c8y-modal>\n","import { Component, OnInit } from '@angular/core';\nimport {\n FormBuilder,\n FormGroup,\n Validators,\n FormsModule,\n ReactiveFormsModule\n} from '@angular/forms';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport {\n ApplicationService,\n ApplicationType,\n BillingMode,\n IApplication,\n IManagedObject,\n InventoryService,\n Isolation\n} from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AlertService,\n GainsightService,\n ModalService,\n Permissions,\n PropertiesListItem,\n ViewContext,\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n AppIconComponent,\n IconDirective,\n TextareaAutoresizeDirective,\n C8yTranslateDirective,\n PropertiesListComponent,\n FormGroupComponent,\n RequiredInputPlaceholderDirective,\n MessagesComponent,\n MessageDirective,\n LoadingComponent,\n C8yTranslatePipe,\n HumanizeAppNamePipe,\n DatePipe\n} from '@c8y/ngx-components';\nimport {\n ApplicationState,\n EcosystemService,\n PRODUCT_EXPERIENCE_ECOSYSTEM,\n packageProperties,\n UploadArchiveComponent\n} from '@c8y/ngx-components/ecosystem/shared';\nimport { TranslateService } from '@ngx-translate/core';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { SubscriptionModalComponent } from './subscription-modal/subscription-modal.component';\nimport { UpdateApplicationModalComponent } from './update-application-modal/update-application-modal.component';\nimport { NgIf, NgClass, NgSwitch, NgSwitchCase, AsyncPipe } from '@angular/common';\nimport { TooltipDirective } from 'ngx-bootstrap/tooltip';\nimport { IconSelectorWrapperComponent } from '@c8y/ngx-components/icon-selector';\nimport { ActivityLogComponent } from '../activity-log/activity-log.component';\n\nconst MICROSERVICES_BASE_PATH = '/ecosystem/microservice/microservices';\nconst APPLICATIONS_BASE_PATH = '/ecosystem/application/applications';\n\n@Component({\n selector: 'c8y-application-properties',\n templateUrl: './application-properties.component.html',\n imports: [\n TitleComponent,\n NgIf,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n NgClass,\n FormsModule,\n ReactiveFormsModule,\n AppIconComponent,\n IconDirective,\n TextareaAutoresizeDirective,\n C8yTranslateDirective,\n TooltipDirective,\n PropertiesListComponent,\n FormGroupComponent,\n RequiredInputPlaceholderDirective,\n NgSwitch,\n NgSwitchCase,\n MessagesComponent,\n MessageDirective,\n IconSelectorWrapperComponent,\n UploadArchiveComponent,\n LoadingComponent,\n ActivityLogComponent,\n C8yTranslatePipe,\n AsyncPipe,\n HumanizeAppNamePipe,\n DatePipe\n ]\n})\nexport class ApplicationPropertiesComponent implements OnInit {\n CURRENT_LOCATION = location.href;\n formGroup: FormGroup;\n application: IApplication;\n binaryMo: IManagedObject;\n singleTenant = false;\n subscription = false;\n canOpenInBrowser: boolean;\n disableOpenInBrowser: boolean;\n canDelete: boolean;\n isOwner: boolean;\n isSubscribed: boolean;\n isPackage: boolean;\n isMicroservice: boolean;\n isFeature: boolean;\n isExternal: boolean;\n isUnpacked: boolean;\n iconMap = {\n HOSTED: 'cloud',\n EXTERNAL: 'external-link-square',\n MICROSERVICE: 'microchip'\n };\n appState: ApplicationState;\n sourcePackage: IApplication;\n isLoading = true;\n isActivityLogSupported: boolean;\n isCustomMicroservice: boolean;\n hasAdminPermissions = false;\n noDescriptionLabel = gettext('No description available.');\n\n breadcrumbConfig: { icon: string; label: string; path: string };\n packageProperties: PropertiesListItem[] = [...packageProperties];\n isUpdateAvailable = false;\n\n private blueprintApplicationVersion: string;\n\n constructor(\n private activatedRoute: ActivatedRoute,\n private ecosystemService: EcosystemService,\n private router: Router,\n private formBuilder: FormBuilder,\n private applicationService: ApplicationService,\n private alertService: AlertService,\n private inventoryService: InventoryService,\n private permissions: Permissions,\n private modalService: ModalService,\n private translate: TranslateService,\n private bsModalService: BsModalService,\n private gainsightService: GainsightService\n ) {}\n\n async ngOnInit() {\n this.hasAdminPermissions = this.permissions.hasRole(\n Permissions.ROLE_APPLICATION_MANAGEMENT_ADMIN\n );\n await this.refresh();\n }\n\n async refresh() {\n await this.load();\n this.isUnpacked = this.ecosystemService.isUnpacked(this.application);\n this.isPackage = this.ecosystemService.isPackage(this.application);\n this.isFeature = this.ecosystemService.isFeature(this.application);\n this.isExternal = this.ecosystemService.isExternal(this.application);\n this.isMicroservice = this.ecosystemService.isMicroservice(this.application);\n this.appState = this.ecosystemService.getAppState(this.application);\n\n if (this.isUnpacked) {\n await this.resolveSourcePackageDetails();\n }\n\n this.setBreadcrumbConfig();\n\n if (this.isCustomMicroservice) {\n this.loadBinaryMo();\n }\n }\n\n async load() {\n this.isLoading = true;\n this.initForm();\n await this.loadApplication();\n this.isLoading = false;\n }\n\n onApplication(app: IApplication) {\n if (app.manifest) {\n this.singleTenant = app.manifest.isolation === Isolation.PER_TENANT;\n this.subscription = app.manifest.billingMode === BillingMode.SUBSCRIPTION;\n }\n }\n\n cancel() {\n if (this.application.type === ApplicationType.MICROSERVICE) {\n this.router.navigateByUrl(MICROSERVICES_BASE_PATH);\n } else {\n this.router.navigateByUrl(APPLICATIONS_BASE_PATH);\n }\n }\n\n openApp(app) {\n this.ecosystemService.openApp(app);\n }\n\n getPackage(entityOrId: string | number | IApplication) {\n return this.applicationService.detail(entityOrId);\n }\n\n async delete() {\n try {\n await this.ecosystemService.deleteApp(this.application);\n if (this.application.type === ApplicationType.MICROSERVICE) {\n this.router.navigateByUrl(MICROSERVICES_BASE_PATH);\n } else {\n this.router.navigateByUrl(APPLICATIONS_BASE_PATH);\n }\n } catch (ex) {\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n async subscribe() {\n const initialState = { application: this.application, isSubscribed: false };\n await this.confirmSubscriptionChange(initialState);\n this.loadApplication();\n }\n\n async unsubscribe() {\n const initialState = { application: this.application, isSubscribed: true };\n await this.confirmSubscriptionChange(initialState);\n this.loadApplication();\n }\n\n async loadApplication() {\n const { id } = this.activatedRoute.snapshot.parent.data.contextData;\n this.application = await this.ecosystemService.getApplication(id);\n if (this.application.type === ApplicationType.MICROSERVICE) {\n this.formGroup.get('name').disable();\n }\n\n const updatedApplication = {\n ...this.application,\n description: this.getDescription(this.application),\n icon: this.application.config?.icon?.class || this.application.manifest?.icon?.class\n };\n\n this.formGroup.patchValue(updatedApplication);\n this.canOpenInBrowser = this.ecosystemService.canOpenAppInBrowser(this.application);\n this.disableOpenInBrowser =\n this.canOpenInBrowser &&\n (await this.ecosystemService.isOverwrittenByCustomApp(this.application));\n this.canDelete = await this.ecosystemService.canDeleteApp(this.application);\n this.isOwner = this.ecosystemService.isOwner(this.application);\n this.isActivityLogSupported = this.isActivityLogSupportedByApp(this.application);\n this.isCustomMicroservice = this.ecosystemService.isCustomMicroservice(this.application);\n this.isSubscribed = await this.ecosystemService.checkIfSubscribed(this.application);\n this.onApplication(this.application);\n }\n\n async save(app: IApplication) {\n const icon = this.formGroup.controls['icon'].value;\n const name = this.formGroup.controls['name'].value;\n if (icon) {\n app = {\n ...app,\n config: { ...this.application.config, icon: { class: icon }, appTitle: name }\n };\n } else if (this.application.config) {\n app = {\n ...app,\n config: { ...this.application.config, appTitle: name }\n };\n }\n app.id = this.application.id;\n try {\n const { data } = await this.ecosystemService.updateApp(app);\n this.formGroup.reset();\n this.loadApplication();\n if (data) {\n this.alertService.success(gettext('Application saved.'));\n }\n } catch (ex) {\n // do nothing\n }\n }\n\n onNewArchive() {\n this.loadBinaryMo();\n }\n\n async updateToLatestVersion() {\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.APPLICATION_PROPERTIES,\n {\n component: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.COMPONENTS.APPLICATION_PROPERTIES,\n action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.UPDATE_AVAILABLE,\n url: this.CURRENT_LOCATION\n }\n );\n\n try {\n const translatedBody = this.translate.instant(\n gettext(\n `You're using the version {{ currentVersion }} of the {{ packageName }} package, the latest version available is {{ latestVersion }}, do you want to update? You can always revert to a previous version in the Activity log panel.`\n ),\n {\n currentVersion: this.application.manifest?.version,\n latestVersion: this.blueprintApplicationVersion,\n packageName: this.sourcePackage.name\n }\n );\n await this.modalService.confirm(gettext('Update application'), translatedBody, 'warning', {\n ok: gettext('Update'),\n cancel: gettext('Cancel')\n });\n } catch {\n // modal canceled\n return;\n }\n\n try {\n await this.bsModalService.show<UpdateApplicationModalComponent>(\n UpdateApplicationModalComponent,\n {\n ariaDescribedby: 'modal-body',\n ariaLabelledBy: 'modal-title',\n class: 'modal-sm',\n ignoreBackdropClick: true,\n initialState: {\n sourcePackage: this.sourcePackage,\n application: this.application\n }\n }\n ).content.result;\n } catch {\n // modal canceled\n return;\n }\n\n await this.refresh();\n this.alertService.success(gettext('Application updated.'));\n }\n\n private getDescription(application: IApplication): string | undefined {\n if (!application) {\n return;\n }\n return application.description || application.manifest?.description;\n }\n\n private async confirmSubscriptionChange(initialState: {\n application: IApplication;\n isSubscribed: boolean;\n }) {\n await (\n this.bsModalService.show(SubscriptionModalComponent, {\n class: 'modal-sm',\n ariaDescribedby: 'modal-body',\n ariaLabelledBy: 'modal-title',\n initialState,\n ignoreBackdropClick: true\n }).content as SubscriptionModalComponent\n ).result;\n }\n\n private async resolveSourcePackageDetails() {\n try {\n this.sourcePackage = (await this.getPackage(this.application.manifest?.source)).data;\n if (this.sourcePackage) {\n this.packageProperties.push({\n label: gettext('Source package'),\n value: this.sourcePackage.name,\n type: 'link',\n action: () =>\n this.router.navigateByUrl(\n ViewContext.Extension.replace(':id', this.sourcePackage.id.toString())\n )\n });\n }\n } catch {\n this.alertService.warning(gettext('Unable to resolve source package.'));\n return;\n }\n\n await this.extractVersionInformation(this.application);\n }\n\n private initForm(): void {\n this.formGroup = this.formBuilder.group({\n id: [{ value: '' }],\n name: [Validators.required, Validators.maxLength(120)],\n key: [Validators.required, Validators.maxLength(120)],\n contextPath: [Validators.required, Validators.maxLength(120)],\n description: ['', Validators.maxLength(200)],\n username: [Validators.required],\n password: [Validators.required],\n externalUrl: [Validators.required],\n icon: [undefined, [Validators.minLength(1)]]\n });\n if (!this.hasAdminPermissions) {\n this.formGroup.disable();\n }\n }\n\n private isActivityLogSupportedByApp(app: IApplication): boolean {\n return (\n this.ecosystemService.isOwner(app) &&\n app.type !== ApplicationType.MICROSERVICE &&\n app.type !== ApplicationType.EXTERNAL\n );\n }\n\n private setBreadcrumbConfig() {\n this.breadcrumbConfig = {\n icon: this.isMicroservice ? 'microchip' : this.isFeature ? 'tab' : 'c8y-modules',\n label: this.isMicroservice\n ? gettext('Microservices')\n : this.isFeature\n ? gettext('Features')\n : gettext('Applications'),\n path: this.isMicroservice\n ? '/ecosystem/microservice/microservices'\n : this.isFeature\n ? 'ecosystem/application/features'\n : 'ecosystem/application/applications'\n };\n }\n\n private async loadBinaryMo() {\n this.binaryMo = (await this.inventoryService.detail(this.application.activeVersionId)).data;\n }\n\n private async extractVersionInformation(application: IApplication) {\n if (!application.manifest.isPackage && !application.manifest.source) {\n return;\n }\n\n const blueprintApplicationId = application.manifest?.source;\n const currentVersion = application.manifest?.version;\n\n try {\n const { data: blueprintApplicationVersions } =\n await this.applicationService.listVersions(blueprintApplicationId);\n const blueprintLatestVersion = this.ecosystemService.getApplicationVersionObjectByTag(\n blueprintApplicationVersions,\n 'latest'\n );\n this.blueprintApplicationVersion = blueprintLatestVersion.version;\n this.isUpdateAvailable = this.ecosystemService.shouldUpgradePackage(\n currentVersion,\n blueprintLatestVersion\n );\n } catch {\n this.alertService.warning(gettext('Unable to resolve versions of source package.'));\n }\n }\n}\n","<c8y-title>{{ application | humanizeAppName | async }}</c8y-title>\n\n<c8y-breadcrumb *ngIf=\"!isMicroservice\">\n <c8y-breadcrumb-item\n [icon]=\"'c8y-atom'\"\n [label]=\"'Ecosystem' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-modules'\"\n [label]=\"'Applications' | translate\"\n [path]=\"'ecosystem/application/applications'\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"breadcrumbConfig?.icon\"\n *ngIf=\"isFeature\"\n [label]=\"breadcrumbConfig?.label\"\n [path]=\"breadcrumbConfig?.path\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"application | humanizeAppName | async\"></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"'Properties' | translate\"></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-breadcrumb *ngIf=\"isMicroservice\">\n <c8y-breadcrumb-item\n [icon]=\"'c8y-atom'\"\n [label]=\"'Ecosystem' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"breadcrumbConfig?.icon\"\n [label]=\"breadcrumbConfig?.label\"\n [path]=\"breadcrumbConfig?.path\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"application | humanizeAppName | async\"></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"'Properties' | translate\"></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<div class=\"row\">\n <div [ngClass]=\"{ 'col-md-8': !isActivityLogSupported, 'col-md-12': isActivityLogSupported }\">\n <div\n class=\"card content-fullpage\"\n *ngIf=\"application\"\n [ngClass]=\"{ 'd-grid grid__col--7-5--md': isActivityLogSupported }\"\n >\n <form\n class=\"d-flex d-col content-fullpage\"\n (ngSubmit)=\"formGroup.valid && save(formGroup.value)\"\n [formGroup]=\"formGroup\"\n novalidate\n >\n <div\n class=\"d-contents\"\n *ngIf=\"!isLoading\"\n >\n <div class=\"card-block separator-bottom large-padding flex-no-shrink\">\n <div class=\"d-flex-md a-i-start text-center text-left-md\">\n <c8y-app-icon\n class=\"icon-48\"\n *ngIf=\"!isPackage && !isFeature && !isMicroservice && !isExternal\"\n [app]=\"application\"\n [contextPath]=\"application.contextPath\"\n [name]=\"application.name\"\n ></c8y-app-icon>\n <i\n class=\"icon-48\"\n c8yIcon=\"big-parcel\"\n *ngIf=\"isPackage\"\n ></i>\n <i\n class=\"icon-48\"\n c8yIcon=\"tab\"\n *ngIf=\"isFeature\"\n ></i>\n <i\n class=\"icon-48\"\n c8yIcon=\"microchip\"\n *ngIf=\"isMicroservice\"\n ></i>\n <i\n class=\"icon-48\"\n c8yIcon=\"globe1\"\n *ngIf=\"isExternal\"\n ></i>\n\n <div class=\"p-t-md-16 p-l-md-16 p-r-md-32 flex-grow\">\n <p class=\"h4 text-medium m-b-8\">{{ application | humanizeAppName | async }}</p>\n <p *ngIf=\"!isOwner\">\n <em class=\"text-muted\">\n {{\n formGroup?.controls?.description?.value || (noDescriptionLabel | translate)\n }}\n </em>\n </p>\n <div\n class=\"form-group m-b-0\"\n *ngIf=\"isOwner\"\n >\n <label\n class=\"editable\"\n [ngClass]=\"{ updated: formGroup?.controls?.description?.dirty }\"\n >\n <textarea\n class=\"form-control no-resize\"\n placeholder=\"{{ noDescriptionLabel | translate }}\"\n name=\"description\"\n c8y-textarea-autoresize\n formControlName=\"description\"\n ></textarea>\n </label>\n </div>\n </div>\n <div class=\"text-right-md m-t-4\">\n <span\n class=\"label\"\n [ngClass]=\"appState?.class\"\n >\n {{ appState?.label | translate }}\n </span>\n <div\n class=\"fit-w m-t-2\"\n *ngIf=\"application.manifest?.version\"\n data-cy=\"application-detail--version\"\n >\n <label\n class=\"text-label-small\"\n translate\n >\n Version:\n </label>\n <small class=\"p-l-4 text-bold\">{{ application.manifest?.version }}</small>\n </div>\n <div\n class=\"fit-w m-t-2\"\n *ngIf=\"!isUnpacked\"\n >\n <label\n class=\"text-label-small\"\n translate\n >\n Creation time:\n </label>\n <small class=\"p-l-4 text-bold\">\n {{ (binaryMo?.creationTime | c8yDate) || '---' }}\n </small>\n </div>\n <div class=\"m-t-8\">\n <button\n class=\"btn btn-default btn-sm\"\n [attr.aria-label]=\"\n 'There\\'s a newer version available, click to update' | translate\n \"\n tooltip=\"{{\n 'There\\'s a newer version available, click to update' | translate\n }}\"\n placement=\"top\"\n type=\"button\"\n *ngIf=\"isUpdateAvailable\"\n (click)=\"updateToLatestVersion()\"\n [delay]=\"300\"\n >\n <i [c8yIcon]=\"'installing-updates'\"></i>\n {{ 'Update available' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ 'Open' | translate }}\"\n type=\"button\"\n (click)=\"openApp(application)\"\n [disabled]=\"disableOpenInBrowser\"\n *ngIf=\"canOpenInBrowser\"\n >\n <i [c8yIcon]=\"'external-link'\"></i>\n {{ 'Open' | translate }}\n </button>\n <div *ngIf=\"canOpenInBrowser && disableOpenInBrowser\">\n <small\n class=\"text-muted\"\n translate\n >\n The application is overwritten by a custom application sharing the same path\n </small>\n </div>\n <span *ngIf=\"isCustomMicroservice\">\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ 'Subscribe' | translate }}\"\n type=\"button\"\n (click)=\"subscribe()\"\n *ngIf=\"!isSubscribed\"\n >\n <i [c8yIcon]=\"'check-circle-o'\"></i>\n {{ 'Subscribe' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ 'Unsubscribe' | translate }}\"\n type=\"button\"\n (click)=\"unsubscribe()\"\n *ngIf=\"isSubscribed\"\n >\n <i [c8yIcon]=\"'minus-circle'\"></i>\n {{ 'Unsubscribe' | translate }}\n </button>\n </span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"inner-scroll bg-level-0 flex-grow\">\n <div class=\"card-block large-padding\">\n <div\n class=\"row p-16\"\n *ngIf=\"isPackage\"\n >\n <c8y-properties-list\n icon=\"info\"\n [title]=\"'Package details' | translate\"\n [data]=\"application.manifest\"\n [properties]=\"packageProperties\"\n [emptyLabel]=\"'---'\"\n ></c8y-properties-list>\n </div>\n <div\n class=\"row p-16\"\n *ngIf=\"sourcePackage\"\n >\n <c8y-properties-list\n icon=\"info\"\n