yunohost-admin/src/js/yunohost/controllers/apps.js

740 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function() {
// Get application context
var app = Sammy.apps['#main'];
var store = app.store;
/**
* Apps
*
*/
// List installed apps
app.get('#/apps', function (c) {
c.api('GET', '/apps?full', {}, function(data) {
var apps = data['apps'];
c.arraySortById(apps);
c.view('app/app_list', {apps: apps});
});
});
function levelToColor(level) {
if (level >= 8) {
return 'best';
}
else if (level > 4) {
return 'success';
}
else if (level >= 1) {
return 'warning';
}
else if (isNaN(level)) {
return 'default';
} else {
return 'danger'
}
}
function stateToColor(state) {
if (state === "high-quality") {
return 'best';
}
else if (state === "working") {
return 'success';
}
else {
return 'danger';
}
}
function maintainedStateToColor(state) {
if ( state === "request_help" ) {
return 'info';
}
else if ( state === "request_adoption" ) {
return 'warning';
}
else if ( state === "orphaned" ) {
return 'danger';
}
else {
return 'success';
}
}
function combineColors(stateColor, levelColor, installable) {
if (stateColor === "danger" || levelColor === "danger") {
return 'danger';
}
else if (stateColor === "warning" || levelColor === "warning" || levelColor === "default") {
return 'warning';
}
else
{
return 'info';
}
}
function extractMaintainer(manifest) {
if (manifest.maintainer === undefined)
{
if ((manifest.developer !== undefined) && (manifest.developer.name !== undefined))
{
return manifest.developer.name;
}
else
{
return "?";
}
}
else if (Array.isArray(manifest.maintainer))
{
maintainers = [];
manifest.maintainer.forEach(function(maintainer) {
if (maintainer.name !== undefined)
{
maintainers.push(maintainer.name);
}
});
return maintainers.join(', ');
}
else if (manifest.maintainer.name !== undefined)
{
return manifest.maintainer.name;
}
else
{
return "?";
}
}
// Display catalog home page where users chooses to browse a specific category
app.get('#/apps/catalog', function (c) {
c.api('GET', '/appscatalog?full&with_categories', {}, function (data) {
c.view('app/app_catalog_home', {categories: data["categories"]}, function() {
// Configure layout / rendering for app-category-cards
$('#category-selector').isotope({
itemSelector: '.app-category-card',
layoutMode: 'fitRows',
transitionDuration: 200
});
});
});
});
// Display app catalog for a specific category
app.get('#/apps/catalog/:category', function (c) {
var category_id = c.params['category'];
c.api('GET', '/appscatalog?full&with_categories', {}, function (data) {
var apps = [];
$.each(data['apps'], function(name, app) {
// Ignore not working apps
if (app.state === 'notworking') { return; }
// Ignore apps not in this category
if ((category_id !== "all") && (app.category !== category_id)) { return; }
app.id = app.manifest.id;
app.level = parseInt(app.level);
if (app.high_quality && app.level > 7)
{
app.state = "high-quality";
}
if ( app.maintained === false )
{
app.maintained = "orphaned";
}
else if ( app.maintained === true )
{
app.maintained = "maintained";
}
app.manifest.maintainer = extractMaintainer(app.manifest);
var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0;
app.installable = (!app.installed || app.manifest.multi_instance)
app.levelFormatted = isNaN(app.level) ? '?' : app.level;
app.levelColor = levelToColor(app.level);
app.stateColor = stateToColor(app.state);
app.maintainedColor = maintainedStateToColor(app.maintained);
app.installColor = combineColors(app.stateColor, app.levelColor);
app.updateDate = app.lastUpdate * 1000 || 0;
app.isSafe = (app.installColor !== 'danger');
app.isWorking = isWorking ? "isworking" : "notFullyWorking";
app.isHighQuality = (app.state === "high-quality") ? "isHighQuality" : "";
app.decentQuality = (app.level > 4)?"decentQuality":"badQuality";
apps.push(app);
});
var category = undefined;
$.each(data['categories'], function(i, this_category) {
if (this_category.id === category_id) { category = this_category; }
});
if (category_id === "all") {
category = {title: y18n.t("all_apps"), icon: "search"};
}
// Sort app list
c.arraySortById(apps);
// setup filtering of apps once the view is loaded
function setupFilterEvents () {
// Uses plugin isotope to filter apps (we could had ordering to)
var cardGrid = jQuery('#apps').isotope({
itemSelector: '.app-card',
layoutMode: 'fitRows',
transitionDuration: 200
});
// Default filter is 'decent quality apps'
cardGrid.isotope({ filter: '.decentQuality' });
$(".subtag-selector button").on("click", function() {
var selector = $(this).parent();
$("button", selector).removeClass("active");
$(this).addClass("active");
cardGrid.isotope({ filter: filterApps });
});
filterApps = function () {
// Check text search
var input = jQuery("#filter-app-cards").val().toLowerCase();
if (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) <= -1) return false;
// Check subtags
var subtag = $(".subtag-selector button.active").data("subtag");
var this_subtags = jQuery(this).data("subtags");
if ((subtag !== undefined) && (subtag !== "all")) {
if ((subtag === "others") && (this_subtags !== "")) return false;
if ((subtag !== "others") && (this_subtags.split(",").indexOf(subtag) <= -1)) return false;
}
// Check quality criteria
var class_ = jQuery("#dropdownFilter").data("filter");
if ((class_ !== '*') && (! jQuery(this).hasClass(class_))) return false;
return true;
},
jQuery('.dropdownFilter').on('click', function() {
// change dropdown label
jQuery('#app-cards-list-filter-text').text(jQuery(this).find('.menu-item').text());
// change filter attribute
jQuery('#dropdownFilter').data("filter", jQuery(this).data("filter"));
// filter !
cardGrid.isotope({ filter: filterApps });
});
jQuery("#filter-app-cards").on("keyup", function() {
cardGrid.isotope({ filter: filterApps });
});
$("#install-custom-app a[role='button']").on('click', function() {
var url = $("#install-custom-app input[name='url']")[0].value;
if (url.indexOf("github.com") < 0) {
return;
}
c.confirm(
y18n.t('applications'),
y18n.t('confirm_install_custom_app'),
function(){
c.redirect_to('#/apps/install/custom/' + encodeURIComponent(url));
}
);
});
};
// render
c.view('app/app_catalog_category', {apps: apps, category: category}, setupFilterEvents);
});
});
// Get app information
app.get('#/apps/:app', function (c) {
c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(data) {
c.api('GET', '/users/permissions', {}, function(data_permissions) {
// Permissions
data.permissions = data_permissions.permissions[c.params['app']+".main"]["allowed"];
// Multilingual description
data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ?
data.manifest.description[y18n.locale] :
data.manifest.description['en']
;
// Multi Instance settings
data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no');
data.install_time = new Date(data.settings.install_time * 1000);
c.view('app/app_info', data, function() {
// Button to set the app as default
$('button[data-action="set-as-default"]').on("click", function() {
var app = $(this).data("app");
c.confirm(
y18n.t('applications'),
y18n.t('confirm_app_default'),
function() { c.api('PUT', '/apps/'+app+'/default', {}, function() { c.refresh() }); }
);
});
// Button to uninstall the app
$('button[data-action="uninstall"]').on("click", function() {
var app = $(this).data("app");
c.confirm(
y18n.t('applications'),
y18n.t('confirm_uninstall', [app]),
function() {
c.api('DELETE', '/apps/'+ app, {}, function() {
c.redirect_to('#/apps');
});
}
);
});
});
});
});
});
//
// App actions
//
// Get app actions list
app.get('#/apps/:app/actions', function (c) {
c.api('GET', '/apps/'+c.params['app']+'/actions', {}, function(data) {
$.each(data.actions, function(_, action) {
formatYunoHostStyleArguments(action.arguments, c.params);
// Multilingual description
if (action.description && Array.isArray(action.description))
action.description = (typeof action.description[y18n.locale] !== 'undefined') ?
action.description[y18n.locale] :
action.description['en']
;
});
c.view('app/app_actions', data);
return;
});
});
// Perform app action
app.put('#/apps/:app/actions/:action', function(c) {
// taken from app install
$.each(c.params, function(k, v) {
if (typeof(v) === 'object' && Array.isArray(v)) {
// And return only first value
c.params[k] = v[0];
}
});
var app_id = c.params['app'];
delete c.params['app'];
var action_id = c.params['action'];
delete c.params['action'];
var params = {
'args': c.serialize(c.params.toHash())
}
c.api('PUT', '/apps/'+app_id+'/actions/'+action_id, params, function() {
c.redirect_to('#/apps/'+app_id+'/actions', {slide:false});
});
});
//
// App config panel
//
// Get app config panel
app.get('#/apps/:app/config-panel', function (c) {
c.api('GET', '/apps/'+c.params['app']+'/config-panel', {}, function(data) {
$.each(data.config_panel.panel, function(_, panel) {
$.each(panel.sections, function(_, section) {
formatYunoHostStyleArguments(section.options, c.params);
});
});
c.view('app/app_config-panel', data);
});
});
app.post('#/apps/:app/config', function(c) {
// taken from app install
$.each(c.params, function(k, v) {
if (typeof(v) === 'object' && Array.isArray(v)) {
// And return only first value
c.params[k] = v[0];
}
});
var app_id = c.params['app'];
delete c.params['app'];
var params = {
'args': c.serialize(c.params.toHash())
}
c.api('POST', '/apps/'+app_id+'/config', params, function() {
c.redirect_to('#/apps/'+app_id+'/config-panel', {slide:false});
});
})
// Helper function that formats YunoHost style arguments for generating a form
function formatYunoHostStyleArguments(args, params) {
if (!args) {
return;
}
// this is in place modification, I don't like it but it was done this way
$.each(args, function(k, v) {
// Default values
args[k].type = (typeof v.type !== 'undefined') ? v.type : 'string';
args[k].inputType = 'text';
args[k].isPassword = false;
args[k].isDisplayText = false;
args[k].required = (typeof v.optional !== 'undefined' && (v.optional == "true" || v.optional == true)) ? '' : 'required';
args[k].attributes = "";
args[k].helpText = "";
args[k].helpLink = "";
// Multilingual label
if (typeof args[k].ask === "string")
{
args[k].label = args[k].ask;
}
else if (typeof args[k].ask[y18n.locale] !== 'undefined') {
args[k].label = args[k].ask[y18n.locale];
}
else {
args[k].label = args[k].ask['en'];
}
// Multilingual help text
if (typeof args[k].help !== 'undefined') {
args[k].helpText = (typeof args[k].help[y18n.locale] !== 'undefined') ?
args[k].help[y18n.locale] :
args[k].help['en']
;
}
// Input with choices becomes select list
if (typeof args[k].choices !== 'undefined') {
// Update choices values with key and checked data
var choices = []
$.each(args[k].choices, function(ck, cv){
// Non key/value choices have numeric key, that we don't want.
if (typeof ck == "number") {
// Key is Value in this case.
ck = cv;
}
choices.push({
value: ck,
label: cv,
selected: (ck == args[k].default) ? true : false,
});
});
args[k].choices = choices;
}
// Special case for domain input.
// Display a list of available domains
if (v.name == 'domain' || args[k].type == 'domain') {
args[k].choices = [];
$.each(params.domains, function(key, domain){
args[k].choices.push({
value: domain,
label: domain,
selected: false
});
});
// Custom help link
args[k].helpLink += "<a href='#/domains'>"+y18n.t('manage_domains')+"</a>";
}
// Special case for admin / user input.
// Display a list of available users
if (v.name == 'admin' || args[k].type == 'user') {
args[k].choices = [];
$.each(params.users, function(username, user){
args[k].choices.push({
value: username,
label: user.fullname+' ('+user.mail+')',
selected: false
});
});
// Custom help link
args[k].helpLink += "<a href='#/users'>"+y18n.t('manage_users')+"</a>";
}
// 'app' type input display a list of available apps
if (args[k].type == 'app') {
args[k].choices = [];
$.each(params.apps, function(key, app){
args[k].choices.push({
value: app.id,
label: app.name,
selected: false
});
});
// Custom help link
args[k].helpLink += "<a href='#/apps'>"+y18n.t('manage_apps')+"</a>";
}
// Boolean fields
if (args[k].type == 'boolean') {
args[k].inputType = 'checkbox';
// Checked or not ?
if (typeof args[k].default !== 'undefined') {
if (args[k].default == true) {
args[k].attributes = 'checked="checked"';
}
}
// 'default' is used as value, so we need to force it for checkboxes.
args[k].default = 1;
// Checkbox should not be required to be unchecked
args[k].required = '';
// Clone a hidden input with empty value
// https://stackoverflow.com/questions/476426/submit-an-html-form-with-empty-checkboxes
var inputClone = {
name : args[k].name,
inputType : 'hidden',
default : 0
};
args.push(inputClone);
}
// 'password' type input.
if (v.name == 'password' || args[k].type == 'password') {
// Change html input type
args[k].inputType = 'password';
args[k].isPassword = true;
}
if (args[k].type == "display_text") {
args[k].isDisplayText = true;
args[k].label = args[k].label.split("\n");
}
});
}
// Helper function that build app installation form
app.helper('appInstallForm', function(appId, manifest, params) {
var data = {
id: appId,
manifest: manifest,
displayLicense: (manifest['license'] !== undefined && manifest['license'] !== 'free')
};
formatYunoHostStyleArguments(manifest.arguments.install, params);
// Multilingual description
if (typeof manifest.description === 'string')
{
data.description = manifest.description;
}
else if (typeof manifest.description[y18n.locale] !== 'undefined')
{
data.description = manifest.description[y18n.locale];
}
else
{
data.description = manifest.description['en'];
}
// Multi Instance settings boolean to text
data.manifest.multi_instance = manifest.multi_instance ? y18n.t('yes') : y18n.t('no');
// View app install form
c.view('app/app_install', data);
return;
});
// App installation form
app.get('#/apps/install/:app', function (c) {
c.api('GET', '/appscatalog?full', {}, function(data) {
var app_name = c.params["app"];
var app_infos = data["apps"][app_name];
if (app_infos['state'] === "validated")
{
app_infos['state'] = "official";
}
var state_color = stateToColor(app_infos['state']);
var level_color = levelToColor(parseInt(app_infos['level']));
var is_safe_for_install_color = combineColors(state_color, level_color);
if ((is_safe_for_install_color === "warning") || (is_safe_for_install_color === "danger"))
{
c.confirm(
y18n.t("applications"),
y18n.t("confirm_install_app_"+is_safe_for_install_color),
function(){
c.appInstallForm(
app_name,
app_infos.manifest,
c.params
);
},
function () {
c.redirect_to('#/apps/catalog');
}
);
}
else
{
c.appInstallForm(
app_name,
app_infos.manifest,
c.params
);
}
});
});
// Install app (POST)
app.post('#/apps', function(c) {
// Warn admin if app is going to be installed on domain root.
if (c.params['path'] !== '/' || confirm(y18n.t('confirm_install_domain_root', [c.params['domain']]))) {
var params = {
label: c.params['label'],
app: c.params['app']
};
// Check for duplicate arg produced by empty checkbox. (See inputClone)
delete c.params['label'];
delete c.params['app'];
$.each(c.params, function(k, v) {
if (typeof(v) === 'object' && Array.isArray(v)) {
// And return only first value
c.params[k] = v[0];
}
});
params['args'] = c.serialize(c.params.toHash());
// Do not pass empty args.
if (params['args'] === "") {
delete params['args'];
}
c.api('POST', '/apps', params, function() {
c.redirect_to('#/apps');
});
}
else {
c.flash('warning', y18n.t('app_install_cancel'));
c.refresh();
}
});
// Install custom app from github
app.get('#/apps/install/custom/:url', function(c) {
// Force trailing slash
url = c.params['url'];
url = url.replace(/\/?$/, '/');
raw_manifest_url = url.replace('github.com', 'raw.githubusercontent.com') + 'master/manifest.json'
// Fetch manifest.json
jQuery.ajax({ url: raw_manifest_url, type: 'GET' })
.done(function(manifest) {
// raw.githubusercontent.com serve content as plain text
manifest = jQuery.parseJSON(manifest) || {};
c.appInstallForm(
url,
manifest,
c.params
);
})
.fail(function(xhr) {
c.flash('fail', y18n.t('app_install_custom_no_manifest'));
c.redirect("#/apps/catalog/");
});
});
// Get app change label page
app.get('#/apps/:app/changelabel', function (c) {
c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) {
data = {
id: c.params['app'],
label: app_data.settings.label,
};
c.view('app/app_changelabel', data);
});
});
// Change app label
app.post('#/apps/:app/changelabel', function (c) {
params = {'new_label': c.params['label']};
c.api('PUT', '/apps/' + c.params['app'] + '/label', params, function(data) {
c.redirect_to('#/apps/'+ c.params['app']);
});
});
// Get app change URL page
app.get('#/apps/:app/changeurl', function (c) {
c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) {
c.api('GET', '/domains', {}, function(domain_data) {
// Display a list of available domains
var domains = [];
$.each(domain_data.domains, function(k, domain) {
domains.push({
value: domain,
label: domain,
// Select current domain
selected: (domain == app_data.settings.domain ? true : false)
});
});
data = {
id: c.params['app'],
label: app_data.manifest.name,
domains: domains,
// Pre-fill with current path
path: app_data.settings.path
};
c.view('app/app_changeurl', data);
});
});
});
// Change app URL
app.post('#/apps/:app/changeurl', function (c) {
c.confirm(
y18n.t('applications'),
y18n.t('confirm_app_change_url', [c.params['app']]),
function() {
params = {'domain': c.params['domain'], 'path': c.params['path']};
c.api('PUT', '/apps/' + c.params['app'] + '/changeurl', params, function(data) {
c.redirect_to('#/apps/'+ c.params['app']);
});
}
);
});
})();