diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 7e221cf1e..d8f19bba9 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -27,49 +27,84 @@ exports.socketio = function (hook_name, args, cb) { io.on('connection', function (socket) { if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; - socket.on("load", function (query) { + socket.on("getInstalled", function (query) { // send currently installed plugins - socket.emit("installed-results", {results: plugins.plugins}); - socket.emit("progress", {progress:1}); + var installed = Object.keys(plugins.plugins).map(function(plugin) { + return plugins.plugins[plugin].package + }) + socket.emit("results:installed", {installed: installed}); }); socket.on("checkUpdates", function() { - socket.emit("progress", {progress:0, message:'Checking for plugin updates...'}); // Check plugins for updates - installer.search({offset: 0, pattern: '', limit: 500}, /*useCache:*/true, function(data) { // hacky - if (!data.results) return; + installer.getAvailablePlugins(/*maxCacheAge:*/60*10, function(er, results) { + if(er) { + console.warn(er); + socket.emit("results:updatable", {updatable: {}}); + return; + } var updatable = _(plugins.plugins).keys().filter(function(plugin) { - if(!data.results[plugin]) return false; - var latestVersion = data.results[plugin]['dist-tags'].latest + if(!results[plugin]) return false; + var latestVersion = results[plugin].version var currentVersion = plugins.plugins[plugin].package.version return semver.gt(latestVersion, currentVersion) }); - socket.emit("updatable", {updatable: updatable}); - socket.emit("progress", {progress:1}); + socket.emit("results:updatable", {updatable: updatable}); }); }) + + socket.on("getAvailable", function (query) { + installer.getAvailablePlugins(/*maxCacheAge:*/false, function (er, results) { + if(er) { + console.error(er) + results = {} + } + socket.emit("results:available", results); + }); + }); socket.on("search", function (query) { - socket.emit("progress", {progress:0, message:'Fetching results...'}); - installer.search(query, true, function (progress) { - if (progress.results) - socket.emit("search-result", progress); - socket.emit("progress", progress); + installer.search(query.searchTerm, /*maxCacheAge:*/60*10, function (er, results) { + if(er) { + console.error(er) + results = {} + } + var res = Object.keys(results) + .map(function(pluginName) { + return results[pluginName] + }) + .filter(function(plugin) { + return !plugins.plugins[plugin.name] + }); + res = sortPluginList(res, query.sortBy, query.sortDir) + .slice(query.offset, query.offset+query.limit); + socket.emit("results:search", {results: res, query: query}); }); }); socket.on("install", function (plugin_name) { - socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."}); - installer.install(plugin_name, function (progress) { - socket.emit("progress", progress); + installer.install(plugin_name, function (er) { + if(er) console.warn(er) + socket.emit("finished:install", {plugin: plugin_name, error: er? er.message : null}); }); }); socket.on("uninstall", function (plugin_name) { - socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."}); - installer.uninstall(plugin_name, function (progress) { - socket.emit("progress", progress); + installer.uninstall(plugin_name, function (er) { + if(er) console.warn(er) + socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null}); }); }); }); } + +function sortPluginList(plugins, property, /*ASC?*/dir) { + return plugins.sort(function(a, b) { + if (a[property] < b[property]) + return dir? -1 : 1; + if (a[property] > b[property]) + return dir? 1 : -1; + // a must be equal to b + return 0; + }) +} \ No newline at end of file diff --git a/src/package.json b/src/package.json index 822676a38..24f15ca9b 100644 --- a/src/package.json +++ b/src/package.json @@ -27,8 +27,7 @@ "nodemailer" : "0.3.x", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1.x", - "npm-registry-client" : "0.2.10", + "npm" : "1.2.x", "ejs" : "0.6.1", "graceful-fs" : "1.1.5", "slide" : "1.1.3", diff --git a/src/static/css/admin.css b/src/static/css/admin.css index b68238425..2260e27df 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -43,7 +43,7 @@ div.innerwrapper { box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2); margin: auto; max-width: 1150px; - min-height: 100%; + min-height: 101%;/*always display a scrollbar*/ } h1 { @@ -102,12 +102,26 @@ input[type="text"] { max-width: 500px; } +.sort { + cursor: pointer; +} +.sort:after { + content: '▲▼' +} +.sort.up:after { + content:'▲' +} +.sort.down:after { + content:'▼' +} + table { border: 1px solid #ddd; border-radius: 3px; border-spacing: 0; width: 100%; margin: 20px 0; + position:relative; /* Allows us to position the loading indicator relative to the table */ } table thead tr { @@ -122,13 +136,40 @@ td, th { display: none; } -#progress { - position: absolute; - bottom: 50px; +#installed-plugins td>div { + position: relative;/* Allows us to position the loading indicator relative to this row */ + display: inline-block; /*make this fill the whole cell*/ + width:100%; } -#progress img { - vertical-align: top; +.messages td>* { + display: none; + text-align: center; +} + +.messages .fetching { + display: block; +} + +.progress { + position: absolute; + top: 0; left: 0; bottom:0; right:0; + padding: auto; + + background: rgb(255,255,255); + display: none; +} + +#search-progress.progress { + padding-top: 20%; + background: rgba(255,255,255,0.7); +} + +.progress * { + display: block; + margin: 0 auto; + text-align: center; + color: #666; } .settings { @@ -147,7 +188,25 @@ a:link, a:visited, a:hover, a:focus { } a:focus, a:hover { - border-bottom: #333333 1px solid; + text-decoration: underline; +} + +.installed-results a:link, +.search-results a:link, +.installed-results a:visited, +.search-results a:visited, +.installed-results a:hover, +.search-results a:hover, +.installed-results a:focus, +.search-results a:focus { + text-decoration: underline; +} + +.installed-results a:focus, +.search-results a:focus, +.installed-results a:hover, +.search-results a:hover { + text-decoration: none; } pre { diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index a973875ce..41affa745 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -12,176 +12,248 @@ $(document).ready(function () { //connect socket = io.connect(url, {resource : resource}).of("/pluginfw/installer"); - $('.search-results').data('query', { - pattern: '', - offset: 0, - limit: 12, - }); - - var doUpdate = false; - - var search = function () { - socket.emit("search", $('.search-results').data('query')); - tasks++; + function search(searchTerm, limit) { + if(search.searchTerm != searchTerm) { + search.offset = 0 + search.results = [] + search.end = false + } + limit = limit? limit : search.limit + search.searchTerm = searchTerm; + socket.emit("search", {searchTerm: searchTerm, offset:search.offset, limit: limit, sortBy: search.sortBy, sortDir: search.sortDir}); + search.offset += limit; + $('#search-progress').show() + } + search.offset = 0; + search.limit = 12; + search.results = []; + search.sortBy = 'name'; + search.sortDir = /*DESC?*/true; + search.end = true;// have we received all results already? + search.messages = { + show: function(msg) { + $('.search-results .messages').show() + $('.search-results .messages .'+msg+'').show() + }, + hide: function(msg) { + $('.search-results .messages').hide() + $('.search-results .messages .'+msg+'').hide() + } } - function updateHandlers() { - $("form").submit(function(){ - var query = $('.search-results').data('query'); - query.pattern = $("#search-query").val(); - query.offset = 0; - search(); - return false; - }); - - $("#search-query").unbind('keyup').keyup(function () { - var query = $('.search-results').data('query'); - query.pattern = $("#search-query").val(); - query.offset = 0; - search(); - }); - - $(".do-install, .do-update").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("install", row.find(".name").text()); - tasks++; - }); - - $(".do-uninstall").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("uninstall", row.find(".name").text()); - tasks++; - }); - - $(".do-prev-page").unbind('click').click(function (e) { - var query = $('.search-results').data('query'); - query.offset -= query.limit; - if (query.offset < 0) { - query.offset = 0; + var installed = { + progress: { + show: function(plugin, msg) { + $('.installed-results .'+plugin+' .progress').show() + $('.installed-results .'+plugin+' .progress .message').text(msg) + if($(window).scrollTop() > $('.'+plugin).offset().top)$(window).scrollTop($('.'+plugin).offset().top-100) + }, + hide: function(plugin) { + $('.installed-results .'+plugin+' .progress').hide() + $('.installed-results .'+plugin+' .progress .message').text('') } - search(); - }); - $(".do-next-page").unbind('click').click(function (e) { - var query = $('.search-results').data('query'); - var total = $('.search-results').data('total'); - if (query.offset + query.limit < total) { - query.offset += query.limit; + }, + messages: { + show: function(msg) { + $('.installed-results .messages').show() + $('.installed-results .messages .'+msg+'').show() + }, + hide: function(msg) { + $('.installed-results .messages').hide() + $('.installed-results .messages .'+msg+'').hide() } - search(); - }); + }, + list: [] } - updateHandlers(); - - var tasks = 0; - socket.on('progress', function (data) { - $("#progress").show(); - $('#progress').data('progress', data.progress); - - var message = "Unknown status"; - if (data.message) { - message = data.message.toString(); - } - if (data.error) { - data.progress = 1; - } - - $("#progress .message").html(message); - - if (data.progress >= 1) { - tasks--; - if (tasks <= 0) { - // Hide the activity indicator once all tasks are done - $("#progress").hide(); - tasks = 0; - } - - if (data.error) { - alert('An error occurred: '+data.error+' -- the server log might know more...'); - }else { - if (doUpdate) { - doUpdate = false; - socket.emit("load"); - tasks++; - } - } - } - }); - - socket.on('search-result', function (data) { - var widget=$(".search-results"); - - widget.data('query', data.query); - widget.data('total', data.total); - - widget.find('.offset').html(data.query.offset); - if (data.query.offset + data.query.limit > data.total){ - widget.find('.limit').html(data.total); - }else{ - widget.find('.limit').html(data.query.offset + data.query.limit); - } - widget.find('.total').html(data.total); - - widget.find(".results *").remove(); - for (plugin_name in data.results) { - var plugin = data.results[plugin_name]; - var row = widget.find(".template tr").clone(); + function displayPluginList(plugins, container, template) { + plugins.forEach(function(plugin) { + var row = template.clone(); for (attr in plugin) { if(attr == "name"){ // Hack to rewrite URLS into name - row.find(".name").html(""+plugin[attr]+""); + row.find(".name").html(""+plugin['name'].substr(3)+""); // remove 'ep_' }else{ row.find("." + attr).html(plugin[attr]); } } - row.find(".version").html( data.results[plugin_name]['dist-tags'].latest ); - - widget.find(".results").append(row); - } - + row.find(".version").html( plugin.version ); + row.addClass(plugin.name) + row.data('plugin', plugin.name) + container.append(row); + }) updateHandlers(); + } + + function sortPluginList(plugins, property, /*ASC?*/dir) { + return plugins.sort(function(a, b) { + if (a[property] < b[property]) + return dir? -1 : 1; + if (a[property] > b[property]) + return dir? 1 : -1; + // a must be equal to b + return 0; + }) + } + + // Infinite scroll + $(window).scroll(checkInfiniteScroll) + function checkInfiniteScroll() { + if(search.end) return;// don't keep requesting if there are no more results + try{ + var top = $('.search-results .results > tr:last').offset().top + if($(window).scrollTop()+$(window).height() > top) search(search.searchTerm) + }catch(e){} + } + + function updateHandlers() { + // Search + $("#search-query").unbind('keyup').keyup(function () { + search($("#search-query").val()); + }); + + // update & install + $(".do-install, .do-update").unbind('click').click(function (e) { + var $row = $(e.target).closest("tr") + , plugin = $row.data('plugin'); + if($(this).hasClass('do-install')) { + $row.remove().appendTo('#installed-plugins') + installed.progress.show(plugin, 'Installing') + }else{ + installed.progress.show(plugin, 'Updating') + } + socket.emit("install", plugin); + installed.messages.hide("nothing-installed") + }); + + // uninstall + $(".do-uninstall").unbind('click').click(function (e) { + var $row = $(e.target).closest("tr") + , pluginName = $row.data('plugin'); + socket.emit("uninstall", pluginName); + installed.progress.show(pluginName, 'Uninstalling') + installed.list = installed.list.filter(function(plugin) { + return plugin.name != pluginName + }) + }); + + // Sort + $('.sort.up').unbind('click').click(function() { + search.sortBy = $(this).text().toLowerCase(); + search.sortDir = false; + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + $('.sort.down, .sort.none').unbind('click').click(function() { + search.sortBy = $(this).text().toLowerCase(); + search.sortDir = true; + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + } + + socket.on('results:search', function (data) { + if(!data.results.length) search.end = true; + search.messages.hide('nothing-found') + search.messages.hide('fetching') + $("#search-query").removeAttr('disabled') + + console.log('got search results', data) + + // add to results + search.results = search.results.concat(data.results); + + // Update sorting head + $('.sort') + .removeClass('up down') + .addClass('none'); + $('.search-results thead th[data-label='+data.query.sortBy+']') + .removeClass('none') + .addClass(data.query.sortDir? 'up' : 'down'); + + // re-render search results + var searchWidget = $(".search-results"); + searchWidget.find(".results *").remove(); + if(search.results.length > 0) { + displayPluginList(search.results, searchWidget.find(".results"), searchWidget.find(".template tr")) + }else { + search.messages.show('nothing-found') + } + $('#search-progress').hide() + checkInfiniteScroll() }); - socket.on('installed-results', function (data) { - $("#installed-plugins *").remove(); + socket.on('results:installed', function (data) { + installed.messages.hide("fetching") + installed.messages.hide("nothing-installed") - for (plugin_name in data.results) { - if (plugin_name == "ep_etherpad-lite") continue; // Hack... - var plugin = data.results[plugin_name]; - var row = $("#installed-plugin-template").clone(); + installed.list = data.installed + sortPluginList(installed.list, 'name', /*ASC?*/true); - for (attr in plugin.package) { - if(attr == "name"){ // Hack to rewrite URLS into name - row.find(".name").html(""+plugin.package[attr]+""); - }else{ - row.find("." + attr).html(plugin.package[attr]); - } - } - $("#installed-plugins").append(row); + // filter out epl + installed.list = installed.list.filter(function(plugin) { + return plugin.name != 'ep_etherpad-lite' + }) + + // remove all installed plugins (leave plugins that are still being installed) + installed.list.forEach(function(plugin) { + $('#installed-plugins .'+plugin.name).remove() + }) + + if(installed.list.length > 0) { + displayPluginList(installed.list, $("#installed-plugins"), $("#installed-plugin-template")); + socket.emit('checkUpdates'); + }else { + installed.messages.show("nothing-installed") } - updateHandlers(); - - socket.emit('checkUpdates'); - tasks++; }); - socket.on('updatable', function(data) { - $('#installed-plugins>tr').each(function(i,tr) { - var pluginName = $(tr).find('.name').text() - - if (data.updatable.indexOf(pluginName) >= 0) { - var actions = $(tr).find('.actions') - actions.append('') - actions.css('width', 200) - } + socket.on('results:updatable', function(data) { + data.updatable.forEach(function(pluginName) { + var $row = $('#installed-plugins > tr.'+pluginName) + , actions = $row.find('.actions') + actions.append('') }) updateHandlers(); }) - socket.emit("load"); - tasks++; - - search(); + socket.on('finished:install', function(data) { + if(data.error) { + alert('An error occured while installing '+data.plugin+' \n'+data.error) + $('#installed-plugins .'+data.plugin).remove() + } + + socket.emit("getInstalled"); + + // update search results + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + + socket.on('finished:uninstall', function(data) { + if(data.error) alert('An error occured while uninstalling the '+data.plugin+' \n'+data.error) + + // remove plugin from installed list + $('#installed-plugins .'+data.plugin).remove() + + socket.emit("getInstalled"); + + // update search results + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + + // init + updateHandlers(); + socket.emit("getInstalled"); + search(''); + + // check for updates every 5mins + setInterval(function() { + socket.emit('checkUpdates'); + }, 1000*60*5) }); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 15d879409..377be35e9 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,118 +1,77 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm"); -var RegClient = require("npm-registry-client") -var registry = new RegClient( -{ registry: "http://registry.npmjs.org" -, cache: npm.cache } -); - -var withNpm = function (npmfn, final, cb) { +var npmIsLoaded = false; +var withNpm = function (npmfn) { + if(npmIsLoaded) return npmfn(); npm.load({}, function (er) { - if (er) return cb({progress:1, error:er}); + if (er) return npmfn(er); + npmIsLoaded = true; npm.on("log", function (message) { - cb({progress: 0.5, message:message.msg + ": " + message.pref}); - }); - npmfn(function (er, data) { - if (er) { - console.error(er); - return cb({progress:1, error: er.message}); - } - if (!data) data = {}; - data.progress = 1; - data.message = "Done."; - cb(data); - final(); + console.log('npm: ',message) }); + npmfn(); }); } -// All these functions call their callback multiple times with -// {progress:[0,1], message:STRING, error:object}. They will call it -// with progress = 1 at least once, and at all times will either -// message or error be present, not both. It can be called multiple -// times for all values of propgress except for 1. - exports.uninstall = function(plugin_name, cb) { - withNpm( - function (cb) { - npm.commands.uninstall([plugin_name], function (er) { + withNpm(function (er) { + if (er) return cb && cb(er); + npm.commands.uninstall([plugin_name], function (er) { + if (er) return cb && cb(er); + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { if (er) return cb(er); - hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { - if (er) return cb(er); - plugins.update(cb); - }); + plugins.update(cb); + hooks.aCallAll("restartServer", {}, function () {}); }); - }, - function () { - hooks.aCallAll("restartServer", {}, function () {}); - }, - cb - ); + }); + }); }; exports.install = function(plugin_name, cb) { - withNpm( - function (cb) { - npm.commands.install([plugin_name], function (er) { + withNpm(function (er) { + if (er) return cb && cb(er); + npm.commands.install([plugin_name], function (er) { + if (er) return cb && cb(er); + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { if (er) return cb(er); - hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { - if (er) return cb(er); - plugins.update(cb); - }); + plugins.update(cb); + hooks.aCallAll("restartServer", {}, function () {}); }); - }, - function () { - hooks.aCallAll("restartServer", {}, function () {}); - }, - cb - ); + }); + }); }; -exports.searchCache = null; +exports.availablePlugins = null; +var cacheTimestamp = 0; -exports.search = function(query, cache, cb) { - withNpm( - function (cb) { - var getData = function (cb) { - if (cache && exports.searchCache) { - cb(null, exports.searchCache); - } else { - registry.get( - "/-/all", 600, false, true, - function (er, data) { - if (er) return cb(er); - exports.searchCache = data; - cb(er, data); - } - ); - } - } - getData( - function (er, data) { - if (er) return cb(er); - var res = {}; - var i = 0; - var pattern = query.pattern.toLowerCase(); - for (key in data) { // for every plugin in the data from npm - if ( key.indexOf(plugins.prefix) == 0 - && key.indexOf(pattern) != -1 - || key.indexOf(plugins.prefix) == 0 - && data[key].description.indexOf(pattern) != -1 - ) { // If the name contains ep_ and the search string is in the name or description - i++; - if (i > query.offset - && i <= query.offset + query.limit) { - res[key] = data[key]; - } - } - } - cb(null, {results:res, query: query, total:i}); - } - ); - }, - function () { }, - cb - ); +exports.getAvailablePlugins = function(maxCacheAge, cb) { + withNpm(function (er) { + if (er) return cb && cb(er); + if(exports.availablePlugins && maxCacheAge && Math.round(+new Date/1000)-cacheTimestamp <= maxCacheAge) { + return cb && cb(null, exports.availablePlugins) + } + npm.commands.search(['ep_'], /*silent?*/true, function(er, results) { + if(er) return cb && cb(er); + exports.availablePlugins = results; + cacheTimestamp = Math.round(+new Date/1000); + cb && cb(null, results) + }) + }); +}; + + +exports.search = function(searchTerm, maxCacheAge, cb) { + exports.getAvailablePlugins(maxCacheAge, function(er, results) { + if(er) return cb && cb(er); + var res = {}; + searchTerm = searchTerm.toLowerCase(); + for (var pluginName in results) { // for every available plugin + if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! + if(pluginName.indexOf(searchTerm) < 0 && results[pluginName].description.indexOf(searchTerm) < 0) continue; + res[pluginName] = results[pluginName]; + } + cb && cb(null, res) + }) }; diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 7c2a7abf2..44e6f7a51 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -28,43 +28,11 @@
  • Troubleshooting information
  • <% e.end_block(); %> -
      
    -

    Installed plugins

    - - - - - - - - - - - - - - - - - - - -
    NameDescriptionVersion
    - -
    - -
    -
    - -

    Available plugins

    -
    - -
    - - +

    Installed plugins

    +
    @@ -74,23 +42,70 @@ - + - + - + + + +
    Name
    - - +
    + +

    +
    +
    +

    You haven't installed any plugins yet.

    +


    Fetching installed plugins...

    +
    - - .. of . - -
    - + +
    +
    + +

    Available plugins

    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionVersion
    +
    + +

    +
    +
    +
    +

    No plugins found.

    +


    Fetching catalogue...

    +
    +
    +