@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,128 lines (1,123 loc) • 185 kB
JavaScript
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 }, {