UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,128 lines (1,123 loc) 185 kB
import * as i0 from '@angular/core'; import { EventEmitter, Injectable, ViewChild, Input, Component, Pipe, Output, forwardRef, inject, NgModule } from '@angular/core'; import * as i3$1 from '@c8y/client'; import { ApplicationAvailability, ApplicationType } from '@c8y/client'; import * as i1 from '@c8y/ngx-components'; import { gettext, PackageType, Status, WizardHeaderComponent, IconDirective, WizardBodyComponent, FormGroupComponent, DropAreaComponent, MessagesComponent, MessageDirective, OperationResultComponent, WizardFooterComponent, C8yTranslateDirective, C8yTranslatePipe, AppIconComponent, IfAllowedDirective, HumanizeAppNamePipe, RequiredInputPlaceholderDirective, LoadingComponent, C8yStepper, TypeaheadComponent, ForOfDirective, ListItemComponent, ListItemIconComponent, HighlightComponent, internalApps, FilterInputComponent, PluginsService, MarkdownToHtmlPipe, EmptyStateComponent, CoreModule, hookWizard } from '@c8y/ngx-components'; import * as i3 from '@ngx-translate/core'; import { saveAs } from 'file-saver'; import { groupBy, uniqBy, get, pick, omit, isUndefined, kebabCase, cloneDeep } from 'lodash-es'; import { BehaviorSubject, defer, merge, combineLatest, Subject, pipe } from 'rxjs'; import { map, shareReplay, debounceTime, take, filter, distinctUntilKeyChanged, tap, switchMap, takeUntil } from 'rxjs/operators'; import { satisfies, gt, coerce, lt } from 'semver'; import { NgIf, NgFor, AsyncPipe, NgClass, NgPlural, NgPluralCase, TitleCasePipe } from '@angular/common'; import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover'; import * as i1$1 from '@angular/forms'; import { FormsModule, Validators, ReactiveFormsModule, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms'; import * as i4 from '@angular/router'; import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip'; import { BsDropdownDirective, BsDropdownToggleDirective, BsDropdownMenuDirective, BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { CdkTrapFocus, A11yModule } from '@angular/cdk/a11y'; import { IconSelectorWrapperComponent, IconSelectorModule } from '@c8y/ngx-components/icon-selector'; import * as i1$2 from 'ngx-bootstrap/modal'; import { CdkStep } from '@angular/cdk/stepper'; /** Wizard types */ var EcosystemWizards; (function (EcosystemWizards) { EcosystemWizards["APPLICATION_UPLOAD"] = "ecosystemApplicationUpload"; EcosystemWizards["MICROSERVICE_UPLOAD"] = "ecosystemMicroserviceUpload"; EcosystemWizards["PACKAGE_UPLOAD"] = "ecosystemPackageUpload"; EcosystemWizards["BLUEPRINT_DEPLOYMENT"] = "ecosystemBlueprintDeployment"; EcosystemWizards["LICENSE_CONFIRM"] = "ecosystemLicenseConfirm"; EcosystemWizards["ARCHIVED_CONFIRM"] = "ecosystemArchivedConfirm"; })(EcosystemWizards || (EcosystemWizards = {})); var ERROR_TYPE; (function (ERROR_TYPE) { ERROR_TYPE["TYPE_VALIDATION"] = "TYPE_VALIDATION"; ERROR_TYPE["ALREADY_SUBSCRIBED"] = "ALREADY_SUBSCRIBED"; ERROR_TYPE["ALREADY_EXIST"] = "ALREADY_EXIST"; ERROR_TYPE["INTERNAL_ERROR"] = "INTERNAL_ERROR"; ERROR_TYPE["NO_MANIFEST_FILE"] = "NO_MANIFEST_FILE"; ERROR_TYPE["INVALID_PACKAGE"] = "INVALID_PACKAGE"; ERROR_TYPE["INVALID_APPLICATION"] = "INVALID_APPLICATION"; ERROR_TYPE["MICROSERVICE_NAME_TOO_LONG"] = "MICROSERVICE_NAME_TOO_LONG"; ERROR_TYPE["APPLICATION_CREATION_FAILED"] = "APPLICATION_CREATION_FAILED"; ERROR_TYPE["KEY_OR_CONTEXT_PATH_MISMATCH"] = "KEY_OR_CONTEXT_PATH_MISMATCH"; ERROR_TYPE["VERSION_NOT_FOUND"] = "VERSION_NOT_FOUND"; })(ERROR_TYPE || (ERROR_TYPE = {})); const PRODUCT_EXPERIENCE_ECOSYSTEM = { APPLICATIONS: { EVENTS: { AVAILABILITY: 'availability', APPLICATION_CARD: 'applicationCard', APPLICATION_PROPERTIES: 'applicationProperties', DEPLOY_APPLICATION: 'deployApplication', DUPLICATE_APPLICATION: 'duplicateApplication', INSTALLED_PLUGINS: 'installedPlugins', PACKAGE_PLUGINS: 'packagePlugins', PACKAGE_VERSIONS: 'packageVersions', FILTER_LIST: 'filterList' }, COMPONENTS: { APPLICATION_CARD: 'application-card', APPLICATION_PLUGINS: 'application-plugins', APPLICATION_PROPERTIES: 'application-properties', DEPLOY_APPLICATION: 'deploy-application', DUPLICATE_APPLICATION_PROPERTIES: 'duplicate-application-properties', PLUGIN_LIST: 'plugin-list', PACKAGE_VERSIONS: 'package-versions-list', UPDATE_PLUGIN_OF_APP: 'update-plugin-of-app', LIST_FILTERS: 'list-filters', PACKAGE_DETAILS: 'package-details' }, ACTIONS: { AVAILABILITY_CHANGE: 'availabilityChange', CANCEL: 'cancel', CLONE: 'clone', CHANGE_PLUGIN_VERSION: 'changePluginVersion', DELETE: 'delete', DOWNLOAD: 'download', DEPLOY_APPLICATION: 'deployApplication', EDIT: 'edit', INSTALL_PLUGIN: 'installPlugin', INSTALL_PLUGINS: 'installPlugins', UNINSTALL_PLUGIN: 'uninstallPlugin', SELECT_VERSION: 'selectVersion', SET_AS_LATEST: 'setAsLatest', UPDATE_AVAILABLE: 'updateAvailable', UPLOAD: 'upload', SET_FILTER_TERM: 'setFilterTerm', SET_PREDEFINED_FILTERS: 'setPredefinedFilters', RESET_FILTER: 'resetFilter', INSTALL_PLUGINS_INITIATED: 'installPluginsInitiated', DEPLOY_APPLICATION_INITIATED: 'deployApplicationInitiated' }, RESULTS: { DEPLOYED: 'deployed', DUPLICATED: 'duplicated', PLUGIN_INSTALLED: 'pluginInstalled', PLUGIN_REMOVED: 'pluginRemoved', PLUGIN_VERSION_CHANGED: 'pluginVersionChanged', SERVER_FAILURE: 'serverFailure', SUCCESS: 'success' } } }; const ERROR_MESSAGES = { [ERROR_TYPE.ALREADY_EXIST]: gettext('Could not deploy the application, as an application with the same name`KEEP_ORIGINAL`, context-path`KEEP_ORIGINAL` or key`KEEP_ORIGINAL` exists already.'), [ERROR_TYPE.TYPE_VALIDATION]: gettext('Wrong file format. Expected a *.zip file with a valid manifest.'), [ERROR_TYPE.ALREADY_SUBSCRIBED]: gettext('Could not subscribe to the microservice because another application with the same context path is already subscribed.'), [ERROR_TYPE.NO_MANIFEST_FILE]: gettext('Could not find a manifest.'), [ERROR_TYPE.INVALID_PACKAGE]: gettext('You have not uploaded a valid package.'), [ERROR_TYPE.INVALID_APPLICATION]: gettext('You have not uploaded a valid application.'), [ERROR_TYPE.INTERNAL_ERROR]: gettext('An internal error occurred, try to upload again.'), [ERROR_TYPE.MICROSERVICE_NAME_TOO_LONG]: gettext('Microservice name "{{ name }}" must not be longer than {{ maxChars }} characters.'), [ERROR_TYPE.APPLICATION_CREATION_FAILED]: gettext('Application creation failed.'), [ERROR_TYPE.KEY_OR_CONTEXT_PATH_MISMATCH]: gettext('The "contextPath`KEEP_ORIGINAL`" or "key`KEEP_ORIGINAL`" of the uploaded archive do not match with the existing application.'), [ERROR_TYPE.VERSION_NOT_FOUND]: gettext('The selected version was not found on the server.') }; const APP_STATE = { SUBSCRIBED: { label: gettext('Subscribed`application`'), class: 'label-primary', tooltip: gettext('Provided by parent tenant.') }, CUSTOM: { label: gettext('Custom`application`'), class: 'label-info', tooltip: gettext('Manually uploaded to the platform.') }, EXTERNAL: { label: gettext('External`application`'), class: 'label-warning', tooltip: gettext('Application hosted outside of the platform.') }, UNPACKED: { label: gettext('Unpacked`application`'), class: 'label-success', tooltip: gettext('Deployed from a package available under "Packages".') }, PACKAGE_BLUEPRINT: { label: gettext('Blueprint'), class: 'label-success', tooltip: gettext('Contains an application and may include plugins.') }, PACKAGE_PLUGIN: { label: gettext('Plugins'), class: 'label-info', tooltip: gettext('Contains only plugins.') }, PACKAGE_UNKNOWN: { label: gettext('Unknown`package-type`'), class: 'label-info', tooltip: gettext('Package contents could not be determined.') } }; const PACKAGE_TYPE_LABELS = { [PackageType.COMMUNITY]: { label: gettext('COMMUNITY`Package created by the developer community.`'), tooltip: gettext('Package created by the developer community.') }, [PackageType.OFFICIAL]: { label: gettext('OFFICIAL`Package maintained by Cumulocity.`'), tooltip: gettext('Package maintained by Cumulocity.') }, [PackageType.UNKNOWN]: { label: gettext('CUSTOM`Package maintained by an unknown source.`'), tooltip: gettext('Package maintainer unknown.') }, [PackageType.ARCHIVED]: { label: gettext('ARCHIVED`Package out of maintenance.`'), tooltip: gettext('The package was marked by the author as archived.') } }; const packageProperties = [ { label: gettext('Latest version'), key: 'version' }, { label: gettext('Author'), key: 'author' }, { label: gettext('Keywords'), key: 'keywords' }, { label: gettext('Source'), key: 'repository', transform: (repository) => (repository?.url ? repository.url : repository), type: 'link', action: (e, link) => window.open(link, '_blank', 'noopener,noreferrer') }, { label: gettext('Homepage'), key: 'homepage', type: 'link', action: (e, link) => window.open(link, '_blank', 'noopener,noreferrer') }, { label: gettext('License'), key: 'license' } ]; class EcosystemError extends Error { constructor(type) { super(ERROR_MESSAGES[type]); this.type = type; } } const CUMULOCITY_JSON = 'cumulocity.json'; const MICROSERVICE_NAME_MAX_LENGTH = 23; class EcosystemService { constructor(modal, alertService, humanizeAppName, translateService, applicationService, appStateService, zipService, tenantService, inventoryService, wizardModalService, pluginService) { this.modal = modal; this.alertService = alertService; this.humanizeAppName = humanizeAppName; this.translateService = translateService; this.applicationService = applicationService; this.appStateService = appStateService; this.zipService = zipService; this.tenantService = tenantService; this.inventoryService = inventoryService; this.wizardModalService = wizardModalService; this.pluginService = pluginService; this.appDeleted = new EventEmitter(); this.progress = new BehaviorSubject(null); this.appsGroupedByContextPath$ = defer(() => this.getWebApplications()).pipe(map(webApps => groupBy(webApps, 'contextPath')), shareReplay({ bufferSize: 1, refCount: true })); } getUniqueAppConfig(srcApp, existingApps) { let app = { name: srcApp.name, key: srcApp.key, contextPath: srcApp.contextPath }; for (let retryNo = 0; retryNo < 9;) { if (this.checkIfAppNameKeyPathExists(existingApps, app, retryNo)) { retryNo++; app = { name: [srcApp.name, retryNo].join('-'), key: [srcApp.key, retryNo].join('-'), contextPath: [srcApp.contextPath, retryNo].join('-') }; } else { return app; } } return app; } /** * Verify versions compatibility for blueprints. If a blueprint version * is not compatible, a warning is shown. * * @param blueprint The blueprint to install. * @returns true if the installation can continue or false if it should be aborted. */ async verifyBlueprintVersionsCompatibility(blueprint) { const api = await this.getPlatformVersion(); if (blueprint.versioningMatrix) { try { const pluginApiVersion = blueprint.versioningMatrix[blueprint.version].api; if (!satisfies(api, pluginApiVersion)) { return await this.showModal(blueprint, false, api); } } catch { return await this.showModal(blueprint, false, api); } } return true; } /** * Verify versions compatibility for plugins. In case a version does not exist in the * versioningMatrix we don't do anything due to backward compatibility. If a plugin version * is not compatible, a warning is shown. * * @param pluginsToInstall The list of plugins to install. * @returns true if the installation can continue or false if it should be aborted. */ async verifyPluginVersionsCompatibility(pluginsToInstall, app) { const api = await this.getPlatformVersion(); const sdk = app.manifest?.webSdkVersion; for (const plugin of pluginsToInstall) { if (plugin.versioningMatrix) { try { const pluginSdkVersion = plugin.versioningMatrix[plugin.version].sdk; const pluginApiVersion = plugin.versioningMatrix[plugin.version].api; if (!satisfies(sdk, pluginSdkVersion) || !satisfies(api, pluginApiVersion)) { return await this.showModal(plugin, true, api, sdk); } } catch { return await this.showModal(plugin, false, api, sdk); } } } return true; } /** * Community plugins need to verify the license agreement. If a package is a community * package, the license is shown. * * @param pluginsToInstall The list of plugins to install. * @returns true if the installation can continue. */ async verifyLicenses(pluginsToInstall) { let _resolve; const result = new Promise(resolve => { _resolve = resolve; }); pluginsToInstall = pluginsToInstall.filter(plugin => plugin.type !== PackageType.CUSTOM && plugin.type !== PackageType.OFFICIAL); if (pluginsToInstall.length === 0) { return Promise.resolve(true); } const initialState = { id: EcosystemWizards.LICENSE_CONFIRM, componentInitialState: { pluginsToInstall } }; const modalOptions = { initialState }; const wizard = this.wizardModalService.show(modalOptions); wizard.content.onClose.subscribe(confirmed => { _resolve(confirmed); }); return result; } /** * If the plugin is archived, a warning should be shown. * * @param pluginsToInstall The list of plugins to install. * @returns true if the installation can continue. */ async verifyArchived(pluginsToInstall) { let _resolve; const result = new Promise(resolve => { _resolve = resolve; }); pluginsToInstall = pluginsToInstall.filter(plugin => plugin.type === PackageType.ARCHIVED); if (pluginsToInstall.length === 0) { return Promise.resolve(true); } const initialState = { id: EcosystemWizards.ARCHIVED_CONFIRM }; const modalOptions = { initialState }; const wizard = this.wizardModalService.show(modalOptions); wizard.content.onClose.subscribe(confirmed => { _resolve(confirmed); }); return result; } /** * @description * Compares currently deployed application version with application version tagged as "latest" * * @param {string} currentApplicationVersion Deployed application version * @param {object} latestApp Latest application version object * * @returns {boolean} Returns true if latest version is greater than current, otherwise false */ shouldUpgradePackage(currentApplicationVersion, latestApp) { const latestApplicationVersion = latestApp?.version; if (!latestApplicationVersion || !currentApplicationVersion) { return false; } return gt(coerce(latestApplicationVersion), coerce(currentApplicationVersion)); } /** * @description * Gets an object that contains searched tag * * @param {array} applicationVersions Array with all available versions * @param {string} tagName Searched tag * * @returns {object} Returns an object with searched tag */ getApplicationVersionObjectByTag(applicationVersions, tagName) { return applicationVersions?.find(element => element.tags.includes(tagName)); } async getApplication(appId) { return (await this.applicationService.detail(appId)).data; } getApplications(customFilter = {}) { const filter = { pageSize: 2000, withTotalPages: true }; Object.assign(filter, customFilter); const currentTenant = this.appStateService.currentTenant.value; return this.applicationService.listByTenant(currentTenant.name, filter); } async getMicroservices() { const apps = (await this.getApplications()).data; const microservices = apps.filter(app => this.isMicroservice(app)); return microservices.sort((a, b) => a.name.localeCompare(b.name)); } async getWebApplications(customFilter = {}) { const apps = (await this.getApplications(customFilter)).data; const webApps = apps.filter(app => this.isApplication(app)); return webApps.sort((a, b) => a.name.localeCompare(b.name)); } async getFeatureApplications(customFilter = {}) { const apps = (await this.getApplications(customFilter)).data; const webApps = apps.filter(app => this.isFeature(app)); return webApps.sort((a, b) => a.name.localeCompare(b.name)); } async getPackageApplications(customFilter = {}) { const filterCallback = app => this.isPackage(app); return this.getApplicationsFiltered(customFilter, filterCallback); } async getHostedAndPackageApplications(customFilter = {}) { const filterCallback = app => this.isPackage(app) || this.isApplication(app); return this.getApplicationsFiltered(customFilter, filterCallback); } async getApplicationsFiltered(customFilter = {}, filterCallback = (app) => !!app) { const filter = Object.assign({}, customFilter); const sharedFilter = Object.assign({ availability: ApplicationAvailability.SHARED, type: 'HOSTED', pageSize: 2000 }, customFilter); const [{ data: apps }, { data: shared }] = await Promise.all([ this.getApplications(filter), this.applicationService.list(sharedFilter) ]); const webApps = [...apps, ...shared].filter(filterCallback); // an app could be subscribed to a tenant, but also have it's availability set to SHARED, in that case it would occur twice. const uniqWebApps = uniqBy(webApps, (app) => app.id); return uniqWebApps.sort((a, b) => a.name.localeCompare(b.name)); } async isMicroserviceHostingAllowed() { const { data: apps } = await this.applicationService.listByName('feature-microservice-hosting'); return !!apps.filter(app => app.owner?.tenant?.id === 'management').length; } canOpenAppInBrowser(app) { const isNotAFeature = !this.isFeature(app); const hasProperType = [ApplicationType.HOSTED, ApplicationType.EXTERNAL].includes(app.type); const isNotPackage = !this.isPackage(app); return isNotAFeature && hasProperType && isNotPackage; } openApp(app) { window.open(this.applicationService.getHref(app), '_blank', 'noopener,noreferrer'); } async canDeleteApp(app) { return (this.isOwner(app) && (!this.isCurrentApp(app) || (await this.hasSubscribedAppParent(app)))); } isOwner(app) { const currentTenant = this.appStateService.currentTenant.value; const appOwner = get(app, 'owner.tenant.id'); return currentTenant?.name === appOwner; } isFeature(app) { return !!app.name.match(/feature-/); } isMicroservice(app) { return app.type === 'MICROSERVICE'; } isExternal(app) { return app.type === 'EXTERNAL'; } isPackage(app) { return app.manifest?.isPackage === true; } isPlugin(app) { return app.manifest?.package === 'plugin'; } cancelAppCreation(app) { if (this.xhr) { this.xhr.abort(); } if (app) { this.applicationService.delete(app); } } updateUploadProgress(event) { if (event.lengthComputable) { const currentProgress = this.progress.value; this.progress.next(currentProgress + (event.loaded / event.total) * (95 - currentProgress)); } } setAppActiveVersion(app, activeVersionId) { return this.applicationService.update({ id: app.id, activeVersionId }); } setPackageVersionTag(app, version, tags) { return this.applicationService.setPackageVersionTag(app, version, tags); } deletePackageVersion(app, params) { return this.applicationService.deleteVersionPackage(app, params); } getHumanizedAppName(app) { return this.humanizeAppName.transform(app.name).pipe(debounceTime(250), take(1)).toPromise(); } createConfig(app, formGroupValue) { const { id, type, availability } = app; let config = pick(formGroupValue, ['name', 'key', 'contextPath']); config = { ...config, isSetup: true, id, type, availability }; return config; } async listArchives(appId) { const filter = { pageSize: 100 }; return (await this.applicationService.binary(appId).list(filter)).data; } async deleteArchive(archive, app) { const humanizedArchiveName = await this.getHumanizedAppName(archive); try { await this.modal.confirm(gettext('Delete archive'), this.translateService.instant(gettext(`You are about to delete archive "{{ humanizedArchiveName }}". Do you want to proceed?`), { humanizedArchiveName }), Status.DANGER, { ok: gettext('Delete'), cancel: gettext('Cancel') }); await this.applicationService.binary(app).delete(archive.id); this.alertService.success(gettext('Archive deleted.')); } catch (ex) { if (ex) { this.alertService.danger(get(ex, 'data.message'), ex.data); } throw new Error('Cancelled'); } } async getArchiveManagedObject(binaryId) { return (await this.inventoryService.detail(binaryId)).data; } async downloadArchive(app, archive) { try { const binary = await this.getBinary(app, archive); const fileBinary = new Blob([binary], { type: 'application/x-zip-compressed' }); saveAs(fileBinary, archive.name); } catch (e) { // empty } } async updateApp(app, deleteOnFailure = false) { try { return await this.applicationService.update(app); } catch (ex) { this.alertError(ex); if (deleteOnFailure) { await this.applicationService.delete(app.id); throw new EcosystemError(ERROR_TYPE.APPLICATION_CREATION_FAILED); } } } async deleteApp(app, silent = false) { const humanizedAppName = await this.getHumanizedAppName(app); if (!silent) { await this.modal.confirm(gettext('Delete application'), this.translateService.instant(gettext(`You are about to delete application "{{ humanizedAppName }}". Do you want to proceed?`), { humanizedAppName }), Status.DANGER, { ok: gettext('Delete'), cancel: gettext('Cancel') }); } await this.applicationService.delete(app.id); if (!silent) { this.alertService.success(gettext('Application deleted.')); } this.appDeleted.emit(app); } async checkIfSubscribed(app) { const currentTenant = await this.tenantService.current(); const subscribedApps = currentTenant.data.applications.references; return subscribedApps.some(application => application.application.id === app.id); } async subscribeApp(app) { const currentTenant = this.appStateService.currentTenant.value; try { await this.tenantService.subscribeApplication(currentTenant, app); this.alertService.success(gettext('Successfully subscribed to application.')); } catch (ex) { this.alertError(ex); } } async unsubscribeApp(app) { const currentTenant = this.appStateService.currentTenant.value; try { await this.tenantService.unsubscribeApplication(currentTenant, app); this.alertService.success(gettext('Successfully unsubscribed from application.')); } catch (ex) { this.alertError(ex); } } async isValidAppType(archive, appType) { try { const currentType = await this.getAppType(archive); if (currentType !== appType) { throw new EcosystemError(ERROR_TYPE.TYPE_VALIDATION); } else { this.progress.next(this.progress.value + 10); return true; } } catch (ex) { throw new EcosystemError(ERROR_TYPE.TYPE_VALIDATION); } } async uploadArchiveToApp(archive, app, isNewVersion = false) { let uploadOverrides; if (isNewVersion) { uploadOverrides = await this.getUploadOverrides(archive, app); } const binaryService = this.applicationService.binary(app); this.xhr = binaryService.uploadWithProgressXhr(archive, this.updateUploadProgress.bind(this), '', uploadOverrides); const binaryMo = await binaryService.getXMLHttpResponse(this.xhr); // TODO commented it due to: https://cumulocity.atlassian.net/browse/MTM-48553 // Add it back when BE fixes issues with activeVersion. // if (isNewVersion) { // return await this.getApplication(app); // } const notInitialPackage = app.applicationVersions?.length > 0; if (notInitialPackage) { return (await this.applicationService.update({ id: app.id })).data; } return (await this.setAppActiveVersion(app, (binaryMo.binaryId || binaryMo.id))).data; } async validateArchiveToAppCompatibility(archive, app) { const appType = await this.getAppType(archive); if (appType !== app.type) { throw new EcosystemError(ERROR_TYPE.INVALID_APPLICATION); } else { this.progress.next(this.progress.value + 10); } if (this.isMicroservice(app)) { return; } const manifest = await this.getCumulocityJson(archive); // A user can upload an app without a Cumulocity JSON file (e.g. a react app). // This is allowed and should not trigger a validation error. if (!manifest) { return; } await this.validatePackageKeyAndContextPath(manifest, app); } async getCumulocityJson(archive) { try { const c8yManifest = await this.getCumulocityJson$(archive).toPromise(); return c8yManifest; } catch (ex) { return null; } } async createAppForArchive(archive, isPackageTypeArchive = false) { let isPackage = false; const appType = await this.getAppType(archive); let appModel = {}; const supportedAppTypes = [ApplicationType.HOSTED, ApplicationType.MICROSERVICE]; if (supportedAppTypes.includes(appType)) { try { appModel = await this.getCumulocityJson$(archive).toPromise(); isPackage = appModel.isPackage; } catch (e) { // do nothing, we allow having HOSTED applications without the manifest file } } const name = this.getBaseNameFromArchiveOrAppModel(archive, appType, appModel); const clearedName = this.removeForbiddenCharacters(name); const key = this.getAppKey(appModel, clearedName); const contextPath = this.getContextPath(appModel, name); const appToSave = { type: appType, name, key, contextPath }; if (isPackageTypeArchive && !isPackage) { throw new EcosystemError(ERROR_TYPE.INVALID_PACKAGE); } else if (!isPackageTypeArchive && isPackage) { throw new EcosystemError(ERROR_TYPE.INVALID_APPLICATION); } else if (this.isNameLengthExceeded(name, appType)) { const error = new Error(); error.name = ERROR_TYPE.MICROSERVICE_NAME_TOO_LONG; error.message = this.translateService.instant(ERROR_MESSAGES[error.name], { name, maxChars: MICROSERVICE_NAME_MAX_LENGTH }); throw error; } return (await this.applicationService.create({ ...appToSave, manifest: { isPackage, ...(appModel?.package && { package: appModel.package }) } })).data; } async reactivateArchive(app) { try { await this.applicationService.reactivateArchive(app.id); this.alertService.success(gettext('Application reactivated.')); } catch (ex) { this.alertError(ex); } } async removeOldestArchive(app, archives) { try { await this.modal.confirm(gettext('Delete oldest archive and continue'), gettext('Up to 6 archives can be saved in the platform. If you upload a new archive, the oldest archive that is not active will be deleted. Do you want to proceed?'), Status.INFO, { ok: gettext('Delete and continue') }); const archiveToDelete = archives[archives.length - 2]; await this.applicationService.binary(app).delete(archiveToDelete.id); this.alertService.success(gettext('Archive deleted.')); } catch (ex) { if (ex) { this.alertError(ex); } else { return Promise.reject('cancelled'); } } } async deployApp(selectedPackage, formGroupValue, model) { // Create new app config const config = this.createConfig(selectedPackage, formGroupValue); const requestedVersion = model.selected.version; let cleanManifest; try { const manifest = await this.applicationService.getAppManifest(selectedPackage, requestedVersion); cleanManifest = omit(manifest, ['name', 'contextPath', 'key']); } catch (ex) { throw new EcosystemError(ERROR_TYPE.VERSION_NOT_FOUND); } config.isSetup = true; config.manifest = cleanManifest; config.availability = ApplicationAvailability.PRIVATE; config.manifest.isPackage = false; config.manifest.source = selectedPackage.id; config.manifest.package = 'blueprint'; // because of a issue with SHARED availability we always should check again // if the app not exist already const allExistingApps = await this.getHostedAndPackageApplications(); const doesAppKeyOrContextPathExist = this.checkIfAppNameKeyPathExists(allExistingApps, config); if (doesAppKeyOrContextPathExist) { throw new EcosystemError(ERROR_TYPE.ALREADY_EXIST); } // Create new app const newApp = (await this.applicationService.create(config)).data; try { // Binary upload can fail if SHARED package // in this case, catch error and fall back to // clone API. await this.uploadBinaryFromOtherPackage(selectedPackage, newApp, model.selected.binaryId, requestedVersion); } catch (error) { if (error?.res?.status === 404) { await this.fallbackToClone(newApp, selectedPackage, requestedVersion); } } // update the icon if a custom one is selected if (formGroupValue.icon) { await this.applicationService.update({ id: newApp.id, config: { icon: formGroupValue.icon } }); } return newApp; } async fallbackToClone(application, selectedPackage, requestedVersion) { let wasSuccess = true; let clonedPkg; try { clonedPkg = (await this.applicationService.clone(selectedPackage, requestedVersion)).data; await this.uploadBinaryFromOtherPackage(selectedPackage, application, clonedPkg.activeVersionId, requestedVersion, clonedPkg); } catch (error) { this.alertError(error); wasSuccess = false; } finally { await this.deleteApp(clonedPkg, true); } return wasSuccess; } async uploadBinaryFromOtherPackage(selectedPackage, applicationToUploadBinaryTo, binaryId, requestedVersion, useBinariesFrom) { const { data: binaryDetails } = await this.inventoryService.detail(binaryId); // Get binary from specific package version const binary = await this.getBinary(useBinariesFrom || selectedPackage, { id: binaryId }); // Create zip const fileBinary = new Blob([binary], { type: binaryDetails.contentType }); const file = new File([fileBinary], binaryDetails.name, { type: binaryDetails.contentType }); // Upload binary to new app await this.uploadArchiveToApp(file, applicationToUploadBinaryTo); // update the app manifest await this.updateAppManifest(applicationToUploadBinaryTo, selectedPackage, requestedVersion); } getAppState(app) { if (!this.isOwner(app)) { return APP_STATE.SUBSCRIBED; } else if (this.isUnpacked(app)) { return APP_STATE.UNPACKED; } else if (app.type === ApplicationType.EXTERNAL) { return APP_STATE.EXTERNAL; } return APP_STATE.CUSTOM; } getPackageContentState(app) { if (!this.isPackage(app)) { return; } if (this.isPackageBlueprint(app)) { return APP_STATE.PACKAGE_BLUEPRINT; } if (this.isPluginsPackage(app)) { return APP_STATE.PACKAGE_PLUGIN; } return APP_STATE.PACKAGE_UNKNOWN; } isPackageBlueprint(app) { return this.isPackage(app) && app.manifest.package === 'blueprint'; } isPluginsPackage(app) { return this.isPackage(app) && app.manifest.package === 'plugin'; } isUnpacked(app) { return !!app.manifest?.source; } hasExports(app) { return !!app.manifest?.exports?.length; } isApplication(app) { return (app.type !== ApplicationType.MICROSERVICE && !this.isFeature(app) && !this.isPackage(app)); } isCustomMicroservice(app) { return this.isOwner(app) && app.type === ApplicationType.MICROSERVICE; } async getBinary(app, archive) { let binary; try { const res = await this.applicationService.binary(app).downloadArchive(archive.id); binary = await res.arrayBuffer(); } catch (ex) { const msg = gettext('Could not get the binary.'); this.alertService.danger(msg); } return binary; } async isOverwrittenByCustomApp(app) { return !this.isOwner(app) && (await this.hasSubscribedAppParent(app)); } async hasSubscribedAppParent(app) { const appsGroupedByContextPath = await this.appsGroupedByContextPath$.pipe(take(1)).toPromise(); return app.contextPath && appsGroupedByContextPath[app.contextPath]?.length === 2; } /** * @deprecated */ setAvailabilityToPrivateIfNotSetAlready(app) { app.availability = ApplicationAvailability.PRIVATE; return app; } /** * Shows an error dialog. * @param error Either a server error or an internal [[EcosystemError]]. */ alertError(error) { if (error instanceof EcosystemError) { this.alertService.danger(error.message); } else { this.alertService.addServerFailure(error); } } async validatePackageKeyAndContextPath(manifest, app) { const contextPath = get(manifest, 'contextPath'); const appKey = get(manifest, 'key'); if (contextPath !== app.contextPath || appKey !== app.key) { throw new EcosystemError(ERROR_TYPE.KEY_OR_CONTEXT_PATH_MISMATCH); } } filterContainString(name, filterTerm) { const term = filterTerm.toLowerCase().trim(); return name && name.toLowerCase().indexOf(term) > -1; } async updateAppManifest(application, selectedPackage, requestedVersion) { const { id } = application; const cleanedApp = this.removeAppProperties(application); let manifest = selectedPackage.manifest || {}; if (requestedVersion) { try { const versionedAppManifest = await this.applicationService.getAppManifest(selectedPackage, requestedVersion); manifest = versionedAppManifest; } catch (ex) { throw new EcosystemError(ERROR_TYPE.VERSION_NOT_FOUND); } } cleanedApp.manifest = manifest; cleanedApp.manifest.isPackage = false; cleanedApp.manifest.source = selectedPackage.id; return await this.applicationService .binary(id) .updateFiles([{ path: CUMULOCITY_JSON, contents: JSON.stringify(cleanedApp) }]); } /** * Creates object containing properties for filtering applications on lists. * * @param {object} app Application to create filter properties from. * @returns {object} Properties to filter by applications list. */ getAppFilterProps(app) { return { type: this.pluginService.getPackageType(app), availability: this.getAppState(app)?.label, content: this.getPackageContentState(app)?.label }; } checkIfAppNameKeyPathExists(existingApps, app, retryNo) { if (isUndefined(retryNo)) { return existingApps.find(existingApp => existingApp.name === app.name || existingApp.key === app.key || existingApp.contextPath === app.contextPath); } return existingApps.find(existingApp => existingApp.name === app.name || existingApp.key === app.key || existingApp.contextPath === app.contextPath || existingApp.name === [app.name, retryNo].join('-') || existingApp.key === [app.key, retryNo].join('-') || existingApp.contextPath === [app.contextPath, retryNo].join('-')); } async showModal(packageType, isPlugin, apiVersion, sdkVersion) { try { const messages = { plugin: { title: gettext('Plugin installation'), body: sdkVersion ? gettext('The current version of the plugin that you are trying to install does not satisfy the requirements for the following API version "{{ apiVersion }}" or SDK version "{{ sdkVersion }}". Do you want to proceed?') : gettext('The current version of the plugin that you are trying to install does not satisfy the requirements for the following API version "{{ apiVersion }}". Do you want to proceed?') }, blueprint: { title: gettext('Blueprint installation'), body: sdkVersion ? gettext('The current version of the blueprint that you are trying to install does not satisfy the requirements for the following API version "{{ apiVersion }}" or SDK version "{{ sdkVersion }}". Do you want to proceed?') : gettext('The current version of the blueprint that you are trying to install does not satisfy the requirements for the following API version "{{ apiVersion }}". Do you want to proceed?') } }; const entityType = isPlugin ? 'plugin' : 'blueprint'; const selectedMessages = messages[entityType]; const translatedBody = this.translateService.instant(selectedMessages.body, { name: packageType.name, apiVersion, sdkVersion }); await this.modal.confirm(selectedMessages.title, translatedBody, 'warning', { ok: gettext('Continue'), cancel: gettext('Cancel') }); } catch { // modal canceled return false; } return true; } async getPlatformVersion() { return await this.appStateService.state$ .pipe(take(1), map(state => state?.versions?.backend), filter(backendVersion => !!backendVersion)) .toPromise(); } getAppKey(appModel, name) { let key = appModel?.key; if (!key) { key = `${kebabCase(name)}-key`; } return key; } getContextPath(appModel, name) { return appModel?.contextPath || name.toLowerCase(); } removeForbiddenCharacters(str) { return str.replace(/[^a-zA-Z0-9-_]/g, ''); } isCurrentApp(app) { const currentApp = this.appStateService.state.app; return currentApp.contextPath === app.contextPath; } getCumulocityJson$(archive) { return this.zipService.getJsonData(archive, { filename: CUMULOCITY_JSON }); } getAppType(archive) { return this.getCumulocityJson$(archive) .toPromise() .then(data => get(data, 'type') || (get(data, 'apiVersion') ? ApplicationType.MICROSERVICE : ApplicationType.HOSTED)) .catch(() => ApplicationType.HOSTED); } getBaseNameFromArchiveOrAppModel(archive, appType, appModel) { let baseName = appModel?.name || archive.name.replace(/\.zip$/i, ''); if (appType === 'MICROSERVICE') { baseName = this.removeVersionFromName(baseName); } return baseName; } removeAppProperties(app) { const tempApp = cloneDeep(app); const propertiesToRemove = ['id', 'owner', 'activeVersionId', 'self', 'type']; propertiesToRemove.forEach(prop => delete tempApp[prop]); return tempApp; } async getUploadOverrides(archive, app) { const { version } = await this.getCumulocityJson$(archive).toPromise(); const isInitialPackage = app.applicationVersions?.length === 0; return { listUrl: 'versions', headers: { Accept: 'application/vnd.com.nsn.cumulocity.applicationVersion+json;charset=UTF-8;ver=0.9' }, bodyFileProperty: 'applicationBinary', requestBody: { applicationVersion: { version, ...(isInitialPackage && { tags: ['latest'] }) } } }; } removeVersionFromName(name) { const versionRegExp = /-\d+\.\d+\.\d+(\.\d+)?(-\d+)?(.*)$/; return name.replace(versionRegExp, ''); } isNameLengthExceeded(name, appType) { return name.length > MICROSERVICE_NAME_MAX_LENGTH && appType === ApplicationType.MICROSERVICE; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EcosystemService, deps: [{ token: i1.ModalService }, { token: i1.AlertService }, { token: i1.HumanizeAppNamePipe }, { token: i3.TranslateService }, { token: i3$1.ApplicationService }, { token: i1.AppStateService }, { token: i1.ZipService }, { token: i3$1.TenantService }, { token: i3$1.InventoryService }, { token: i1.WizardModalService }, { token: i1.PluginsService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EcosystemService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EcosystemService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.ModalService }, { type: i1.AlertService }, { type: i1.HumanizeAppNamePipe }, { type: i3.TranslateService }, { type: i3$1.ApplicationService }, { type: i1.AppStateService }, { type: i1.ZipService }, { type: i3$1.TenantService }, { type: i3$1.InventoryService }, { type: i1.WizardModalService }, { type: i1.PluginsService }] }); class PackageAvailabilityService { constructor(appState, alert, modal, application, gainsightService, translateService) { this.appState = appState; this.alert = alert; this.modal = modal; this.application = application; this.gainsightService = gainsightService; this.translateService = translateService; this.CURRENT_LOCATION = location.href; this.availabilities = [ { label: gettext('Private`package availability`'), value: ApplicationAvailability.PRIVATE }, { label: gettext('Market`package availability`'), value: ApplicationAvailability.MARKET }, { label: gettext('Shared`package availability`'), value: ApplicationAvailability.SHARED } ]; } async askIfAvailabilityShouldBeSetTo(applicationPackage, availability) { // availability does not matter for tenant that do not have or can create subtenants if (!this.appState.currentTenant.value?.allowCreateTenants) { return applicationPackage; } try { await this.openAvailabilityModal(availability); // user confirmed changing availability return await this.setAvailability(applicationPackage, availability); } catch { // user canceled changing availability return applicationPackage; } } async setAvailability(applicationPackage, availability) { try { const { data: app } = await this.application.updateApplicationAvailability(applicationPackage, availability); this.alert.success(gettext('Updated package availability.')); this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.AVAILABILITY, { action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.AVAILABILITY_CHANGE, result: availability.toString().toLocaleLowerCase(), url: this.CURRENT_LOCATION }); return app; } catch (e) { this.alert.warning(gettext('Failed to set package availability.')); this.alert.addServerFailure(e); this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.EVENTS.AVAILABILITY, { action: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.ACTIONS.AVAILABILITY_CHANGE, result: PRODUCT_EXPERIENCE_ECOSYSTEM.APPLICATIONS.RESULTS.SERVER_FAILURE, url: this.CURRENT_LOCATION }); return applicationPackage; } } getConfirmationBody(availability) { switch (availability) { case ApplicationAvailability.SHARED: return gettext('Do you want to set the package availability to "Shared"? This will make the package available to all subtenants without explicitly subscribing the package.'); case ApplicationAvailability.MARKET: return gettext('Do you want to set the package availability to "Market"? This will make the package available to your own tenant and needs to be subscribed to subtenants individually.'); case ApplicationAvailability.PRIVATE: return gettext('Do you want to set the package availability to "Private"? This will make the package only available to your own tenant.'); default: return ''; } } async openAvailabilityModal(availability) { const body = this.getConfirmationBody(availability); const availabilityLabel = this.availabilities.find(obj => obj.value === availability).label; await this.modal.confirm(gettext('Package availability'), body, 'info', { ok: this.translateService.instant(gettext('Set to "{{ packageAvailability }}"'), { packageAvailability: this.translateService.instant(availabilityLabel) }), cancel: gettext('Cancel') }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PackageAvailabilityService, deps: [{ token: i1.AppStateService }, { token: i1.AlertService }, {