You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
12 KiB
502 lines
12 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { ReactiveDict } from 'meteor/reactive-dict';
|
|
import { FlowRouter } from 'meteor/kadira:flow-router';
|
|
import { Template } from 'meteor/templating';
|
|
import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n';
|
|
import { Tracker } from 'meteor/tracker';
|
|
import _ from 'underscore';
|
|
|
|
import { SideNav } from '../../../ui-utils/client';
|
|
import { isEmail } from '../../../utils';
|
|
import { Utilities } from '../../lib/misc/Utilities';
|
|
import { AppEvents } from '../communication';
|
|
import { Apps } from '../orchestrator';
|
|
import {
|
|
appButtonProps,
|
|
appStatusSpanProps,
|
|
formatPrice,
|
|
formatPricingPlan,
|
|
handleAPIError,
|
|
triggerAppPopoverMenu,
|
|
promptSubscription,
|
|
warnStatusChange,
|
|
checkCloudLogin,
|
|
} from './helpers';
|
|
|
|
import './appManage.html';
|
|
import './appManage.css';
|
|
|
|
const attachAPIs = async (appId, state) => {
|
|
try {
|
|
const apis = await Apps.getAppApis(appId);
|
|
state.set('apis', apis);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
}
|
|
};
|
|
|
|
const attachSettings = async (appId, state) => {
|
|
try {
|
|
const settings = await Apps.getAppSettings(appId);
|
|
|
|
for (const setting of Object.values(settings)) {
|
|
setting.i18nPlaceholder = setting.i18nPlaceholder || ' ';
|
|
setting.value = setting.value !== undefined && setting.value !== null ? setting.value : setting.packageValue;
|
|
setting.oldValue = setting.value;
|
|
setting.hasChanged = false;
|
|
}
|
|
|
|
state.set('settings', settings);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
}
|
|
};
|
|
|
|
const attachBundlesApps = (bundledIn, _app) => {
|
|
if (!bundledIn || !bundledIn.length) {
|
|
return;
|
|
}
|
|
|
|
bundledIn.forEach(async (bundle, i) => {
|
|
try {
|
|
const apps = await Apps.getAppsOnBundle(bundle.bundleId);
|
|
bundle.apps = apps.slice(0, 4);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
}
|
|
|
|
bundledIn[i] = bundle;
|
|
_app.set('bundledIn', bundledIn);
|
|
});
|
|
};
|
|
|
|
const attachMarketplaceInformation = async (appId, version, _app) => {
|
|
try {
|
|
const {
|
|
categories,
|
|
isPurchased,
|
|
price,
|
|
bundledIn,
|
|
purchaseType,
|
|
subscriptionInfo,
|
|
version: marketplaceVersion,
|
|
} = await Apps.getLatestAppFromMarketplace(appId, version);
|
|
|
|
_app.set({
|
|
categories,
|
|
isPurchased,
|
|
price,
|
|
bundledIn,
|
|
purchaseType,
|
|
subscriptionInfo,
|
|
marketplaceVersion,
|
|
});
|
|
|
|
attachBundlesApps(bundledIn, _app);
|
|
} catch (error) {
|
|
if (error.xhr && error.xhr.status === 404) {
|
|
return;
|
|
}
|
|
|
|
handleAPIError(error);
|
|
}
|
|
};
|
|
|
|
const loadApp = async ({ appId, version, state, _app }) => {
|
|
let app;
|
|
try {
|
|
app = await Apps.getApp(appId);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
|
|
state.set('settings', {});
|
|
|
|
if (app) {
|
|
state.set('isLoading', false);
|
|
_app.clear();
|
|
_app.set({ ...app, installed: true });
|
|
|
|
attachAPIs(appId, state);
|
|
attachSettings(appId, state);
|
|
attachMarketplaceInformation(appId, version, _app);
|
|
|
|
if (FlowRouter.current().route.getRouteName() === 'marketplace-app') {
|
|
FlowRouter.withReplaceState(() => {
|
|
FlowRouter.go('app-manage', { appId });
|
|
});
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
app = await Apps.getAppFromMarketplace(appId, version);
|
|
} catch (error) {
|
|
state.set('error', error);
|
|
}
|
|
|
|
if (app) {
|
|
delete app.status;
|
|
app.marketplaceVersion = app.version;
|
|
state.set('isLoading', false);
|
|
_app.clear();
|
|
_app.set({ ...app, installed: false });
|
|
|
|
attachBundlesApps(app.bundledIn, _app);
|
|
|
|
if (FlowRouter.current().route.getRouteName() === 'app-manage') {
|
|
FlowRouter.withReplaceState(() => {
|
|
FlowRouter.go('marketplace-app', { appId });
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
Template.appManage.onCreated(function() {
|
|
this.appId = FlowRouter.getParam('appId');
|
|
this.version = FlowRouter.getQueryParam('version');
|
|
this.state = new ReactiveDict({
|
|
settings: {},
|
|
isLoading: true,
|
|
isSaving: false,
|
|
});
|
|
this._app = new ReactiveDict({
|
|
id: this.appId,
|
|
version: this.version,
|
|
});
|
|
|
|
loadApp(this);
|
|
|
|
this.__ = (key, options, lang_tag) => {
|
|
const appKey = Utilities.getI18nKeyForApp(key, this.appId);
|
|
return TAPi18next.exists(`project:${ appKey }`)
|
|
? TAPi18n.__(appKey, options, lang_tag)
|
|
: TAPi18n.__(key, options, lang_tag);
|
|
};
|
|
|
|
const withAppIdFilter = (f) => (maybeAppId, ...args) => {
|
|
const appId = maybeAppId.appId || maybeAppId;
|
|
if (appId !== this.appId) {
|
|
return;
|
|
}
|
|
|
|
f.call(this, maybeAppId, ...args);
|
|
};
|
|
|
|
this.handleSettingUpdated = withAppIdFilter(() => {
|
|
attachSettings(this.appId, this.state);
|
|
});
|
|
|
|
this.handleChange = withAppIdFilter(() => {
|
|
loadApp(this);
|
|
});
|
|
|
|
this.handleRemoved = withAppIdFilter(() => FlowRouter.go('/admin/apps'));
|
|
|
|
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleChange);
|
|
Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleChange);
|
|
Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleRemoved);
|
|
Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleChange);
|
|
Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, this.handleSettingUpdated);
|
|
});
|
|
|
|
Template.apps.onDestroyed(function() {
|
|
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleChange);
|
|
Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleChange);
|
|
Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleRemoved);
|
|
Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleChange);
|
|
Apps.getWsListener().unregisterListener(AppEvents.APP_SETTING_UPDATED, this.handleSettingUpdated);
|
|
});
|
|
|
|
Template.appManage.onRendered(() => {
|
|
Tracker.afterFlush(() => {
|
|
SideNav.setFlex('adminFlex');
|
|
SideNav.openFlex();
|
|
});
|
|
});
|
|
|
|
Template.appManage.helpers({
|
|
isSettingsPristine() {
|
|
const settings = Template.instance().state.get('settings');
|
|
return !Object.values(settings).some(({ hasChanged }) => hasChanged);
|
|
},
|
|
isSaving() {
|
|
return Template.instance().state.get('isSaving');
|
|
},
|
|
error() {
|
|
const error = Template.instance().state.get('error');
|
|
|
|
return error && (
|
|
(error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error)
|
|
|| error.message
|
|
);
|
|
},
|
|
isLoading() {
|
|
return Template.instance().state.get('isLoading');
|
|
},
|
|
appButtonProps,
|
|
appStatusSpanProps,
|
|
priceDisplay() {
|
|
const [purchaseType, price, pricingPlans] = [
|
|
Template.instance()._app.get('purchaseType'),
|
|
Template.instance()._app.get('price'),
|
|
Template.instance()._app.get('pricingPlans'),
|
|
];
|
|
if (purchaseType === 'subscription') {
|
|
if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) {
|
|
return;
|
|
}
|
|
|
|
return formatPricingPlan(pricingPlans[0]);
|
|
}
|
|
|
|
if (price > 0) {
|
|
return formatPrice(price);
|
|
}
|
|
|
|
return 'Free';
|
|
},
|
|
isEmail,
|
|
_(key, ...args) {
|
|
const [i18nArgs, keyword] = [args.slice(-2), args.slice(-1)[0]];
|
|
|
|
return Template.instance().__(key, {
|
|
...keyword.hash,
|
|
sprintf: i18nArgs,
|
|
});
|
|
},
|
|
languages() {
|
|
return [
|
|
{
|
|
key: '',
|
|
name: 'Default',
|
|
en: 'Default',
|
|
},
|
|
...Object.entries(TAPi18n.getLanguages())
|
|
.map(([key, language]) => ({ key, ...language }))
|
|
.sort(({ key: a }, { key: b }) => a.localeCompare(b)),
|
|
];
|
|
},
|
|
selectedOption(_id, val) {
|
|
const settings = Template.instance().state.get('settings');
|
|
return settings[_id].value === val;
|
|
},
|
|
app() {
|
|
return Template.instance()._app.all();
|
|
},
|
|
errors() {
|
|
const { errors = {} } = Template.instance()._app.get('licenseValidation') || {};
|
|
return Object.values(errors);
|
|
},
|
|
warnings() {
|
|
const { warnings = {} } = Template.instance()._app.get('licenseValidation') || {};
|
|
return Object.values(warnings);
|
|
},
|
|
settings() {
|
|
return Object.values(Template.instance().state.get('settings'));
|
|
},
|
|
apis() {
|
|
return Template.instance().state.get('apis');
|
|
},
|
|
curl(method, api) {
|
|
const example = api.examples[method] || {};
|
|
return Utilities.curl({
|
|
url: Meteor.absoluteUrl.defaultOptions.rootUrl + api.computedPath,
|
|
method,
|
|
params: example.params,
|
|
query: example.query,
|
|
content: example.content,
|
|
headers: example.headers,
|
|
}).split('\n');
|
|
},
|
|
renderMethods(methods) {
|
|
return methods.join('|').toUpperCase();
|
|
},
|
|
bundleAppNames(apps) {
|
|
return apps.map((app) => app.latest.name).join(', ');
|
|
},
|
|
});
|
|
|
|
Template.appManage.events({
|
|
'click .js-cancel-editing-settings'(event, instance) {
|
|
const settings = instance.state.get('settings');
|
|
|
|
for (const setting of Object.values(settings)) {
|
|
setting.value = setting.oldValue;
|
|
setting.hasChanged = false;
|
|
}
|
|
|
|
instance.state.set('settings', settings);
|
|
},
|
|
|
|
async 'click .js-save-settings'(event, instance) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const { appId, state } = instance;
|
|
|
|
if (state.get('isSaving')) {
|
|
return;
|
|
}
|
|
|
|
state.set('isSaving', true);
|
|
|
|
const settings = state.get('settings');
|
|
|
|
try {
|
|
const toSave = Object.values(settings)
|
|
.filter(({ hasChanged }) => hasChanged);
|
|
|
|
if (!toSave.length) {
|
|
return;
|
|
}
|
|
|
|
const updated = await Apps.setAppSettings(appId, toSave);
|
|
updated.forEach(({ id, value }) => {
|
|
settings[id].value = value;
|
|
settings[id].oldValue = value;
|
|
settings[id].hasChanged = false;
|
|
});
|
|
|
|
state.set('settings', settings);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
} finally {
|
|
state.set('isSaving', false);
|
|
}
|
|
},
|
|
'click .js-close'() {
|
|
if (FlowRouter.current().route.getRouteName() === 'marketplace-app') {
|
|
FlowRouter.go('marketplace');
|
|
return;
|
|
}
|
|
|
|
if (FlowRouter.current().route.getRouteName() === 'app-manage') {
|
|
FlowRouter.go('apps');
|
|
return;
|
|
}
|
|
|
|
window.history.back();
|
|
},
|
|
'click .js-menu'(event, instance) {
|
|
event.stopPropagation();
|
|
const { currentTarget } = event;
|
|
|
|
triggerAppPopoverMenu(instance._app.all(), currentTarget, instance);
|
|
},
|
|
|
|
async 'click .js-install, click .js-update'(event, instance) {
|
|
event.stopPropagation();
|
|
|
|
if (!await checkCloudLogin()) {
|
|
return;
|
|
}
|
|
|
|
const { appId, _app } = instance;
|
|
|
|
_app.set('working', true);
|
|
|
|
try {
|
|
const { status } = await Apps.installApp(appId, _app.get('marketplaceVersion'));
|
|
warnStatusChange(_app.get('name'), status);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
} finally {
|
|
_app.set('working', false);
|
|
}
|
|
},
|
|
|
|
async 'click .js-purchase'(event, instance) {
|
|
const { _app } = instance;
|
|
|
|
if (!await checkCloudLogin()) {
|
|
return;
|
|
}
|
|
|
|
_app.set('working', true);
|
|
|
|
const app = _app.all();
|
|
|
|
await promptSubscription(app, async () => {
|
|
try {
|
|
const { status } = await Apps.installApp(app.id, app.marketplaceVersion);
|
|
warnStatusChange(app.name, status);
|
|
} catch (error) {
|
|
handleAPIError(error);
|
|
} finally {
|
|
_app.set('working', false);
|
|
}
|
|
}, () => _app.set('working', false));
|
|
},
|
|
|
|
'change input[type="checkbox"]'(event, instance) {
|
|
const { id } = this;
|
|
const { state } = instance;
|
|
|
|
const settings = state.get('settings');
|
|
const setting = settings[id];
|
|
|
|
if (!setting) {
|
|
return;
|
|
}
|
|
|
|
const value = event.currentTarget.checked;
|
|
|
|
setting.value = value;
|
|
setting.hasChanged = setting.oldValue !== setting.value;
|
|
|
|
state.set('settings', settings);
|
|
},
|
|
|
|
'change .rc-select__element'(event, instance) {
|
|
const { id } = this;
|
|
const { state } = instance;
|
|
|
|
const settings = state.get('settings');
|
|
const setting = settings[id];
|
|
|
|
if (!setting) {
|
|
return;
|
|
}
|
|
|
|
const { value } = event.currentTarget;
|
|
|
|
setting.value = value;
|
|
setting.hasChanged = setting.oldValue !== setting.value;
|
|
|
|
state.set('settings', settings);
|
|
},
|
|
|
|
'input input, input textarea, change input[type="color"]': _.throttle(function(event, instance) {
|
|
const { type, id } = this;
|
|
const { state } = instance;
|
|
|
|
const settings = state.get('settings');
|
|
const setting = settings[id];
|
|
|
|
if (!setting) {
|
|
return;
|
|
}
|
|
|
|
let value = event.currentTarget.value.trim();
|
|
|
|
switch (type) {
|
|
case 'int':
|
|
value = parseInt(value);
|
|
break;
|
|
case 'boolean':
|
|
value = value === '1';
|
|
break;
|
|
case 'code':
|
|
value = $(`.code-mirror-box[data-editor-id="${ id }"] .CodeMirror`)[0].CodeMirror.getValue();
|
|
break;
|
|
}
|
|
|
|
setting.value = value;
|
|
setting.hasChanged = setting.oldValue !== setting.value;
|
|
|
|
state.set('settings', settings);
|
|
}, 500),
|
|
});
|
|
|