From c6ce80e36f2e70c941492254f0ac2e8f159cb832 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 24 Feb 2020 16:23:49 +0100 Subject: luci-app-opkg: full convert to client side actions Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/view/opkg.js | 302 +++++++++++++++++---- 1 file changed, 252 insertions(+), 50 deletions(-) (limited to 'applications/luci-app-opkg/htdocs') diff --git a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js index c7cb55fc47..59ca2cd951 100644 --- a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js +++ b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js @@ -1,3 +1,95 @@ +'use strict'; +'require fs'; +'require ui'; +'require rpc'; + +var css = ' \ + .controls { \ + display: flex; \ + margin: .5em 0 1em 0; \ + flex-wrap: wrap; \ + justify-content: space-around; \ + } \ + \ + .controls > * { \ + padding: .25em; \ + white-space: nowrap; \ + flex: 1 1 33%; \ + box-sizing: border-box; \ + display: flex; \ + flex-wrap: wrap; \ + } \ + \ + .controls > *:first-child, \ + .controls > * > label { \ + flex-basis: 100%; \ + min-width: 250px; \ + } \ + \ + .controls > *:nth-child(2), \ + .controls > *:nth-child(3) { \ + flex-basis: 20%; \ + } \ + \ + .controls > * > .btn { \ + flex-basis: 20px; \ + text-align: center; \ + } \ + \ + .controls > * > * { \ + flex-grow: 1; \ + align-self: center; \ + } \ + \ + .controls > div > input { \ + width: auto; \ + } \ + \ + .td.version, \ + .td.size { \ + white-space: nowrap; \ + } \ + \ + ul.deps, ul.deps ul, ul.errors { \ + margin-left: 1em; \ + } \ + \ + ul.deps li, ul.errors li { \ + list-style: none; \ + } \ + \ + ul.deps li:before { \ + content: "↳"; \ + display: inline-block; \ + width: 1em; \ + margin-left: -1em; \ + } \ + \ + ul.deps li > span { \ + white-space: nowrap; \ + } \ + \ + ul.errors li { \ + color: #c44; \ + font-size: 90%; \ + font-weight: bold; \ + padding-left: 1.5em; \ + } \ + \ + ul.errors li:before { \ + content: "⚠"; \ + display: inline-block; \ + width: 1.5em; \ + margin-left: -1.5em; \ + } \ +'; + +var callMountPoints = rpc.declare({ + object: 'luci', + method: 'getMountPoints', + expect: { result: [] } +}); + var packages = { available: { providers: {}, pkgs: {} }, installed: { providers: {}, pkgs: {} } @@ -471,6 +563,8 @@ function renderDependencies(depends, info) info.seen = info.seen || []; for (var i = 0; i < deps.length; i++) { + var dep, vop, ver; + if (deps[i] === 'libc') continue; @@ -580,7 +674,7 @@ function handleInstall(ev) ]); } - L.showModal(_('Details for package %h').format(pkg.name), [ + ui.showModal(_('Details for package %h').format(pkg.name), [ E('ul', {}, [ E('li', '%s: %h'.format(_('Version'), pkg.version)), E('li', '%s: %h'.format(_('Size'), size)), @@ -595,7 +689,7 @@ function handleInstall(ev) ]), E('div', { 'class': 'btn', - 'click': L.hideModal + 'click': ui.hideModal }, _('Cancel')), ' ', E('div', { @@ -635,11 +729,11 @@ function handleManualInstall(ev) warning = E('p', {}, _('Really attempt to install %h?').format(name_or_url)); } - L.showModal(_('Manually install package'), [ + ui.showModal(_('Manually install package'), [ warning, E('div', { 'class': 'right' }, [ E('div', { - 'click': L.hideModal, + 'click': ui.hideModal, 'class': 'btn cbi-button-neutral' }, _('Cancel')), ' ', install @@ -649,11 +743,29 @@ function handleManualInstall(ev) function handleConfig(ev) { - L.showModal(_('OPKG Configuration'), [ + var conf = {}; + + ui.showModal(_('OPKG Configuration'), [ E('p', { 'class': 'spinning' }, _('Loading configuration data…')) ]); - L.get('admin/system/opkg/config', null, function(xhr, conf) { + fs.list('/etc/opkg').then(function(partials) { + var files = [ '/etc/opkg.conf' ]; + + for (var i = 0; i < partials.length; i++) + if (partials[i].type == 'file' && partials[i].name.match(/\.conf$/)) + files.push('/etc/opkg/' + partials[i].name); + + return Promise.all(files.map(function(file) { + return fs.read(file) + .then(L.bind(function(conf, file, res) { conf[file] = res }, this, conf, file)) + .catch(function(err) { + ui.addNotification(null, E('p', {}, [ _('Unable to read %s: %s').format(file, err) ])); + ui.hideModal(); + throw err; + }); + })); + }).then(function() { var body = [ E('p', {}, _('Below is a listing of the various configuration files used by opkg. Use opkg.conf for global settings and customfeeds.conf for custom repository entries. The configuration in the other files may be changed but is usually not preserved by sysupgrade.')) ]; @@ -669,7 +781,7 @@ function handleConfig(ev) body.push(E('div', { 'class': 'right' }, [ E('div', { 'class': 'btn cbi-button-neutral', - 'click': L.hideModal + 'click': ui.hideModal }, _('Cancel')), ' ', E('div', { @@ -681,16 +793,20 @@ function handleConfig(ev) data[textarea.getAttribute('name')] = textarea.value }); - L.showModal(_('OPKG Configuration'), [ + ui.showModal(_('OPKG Configuration'), [ E('p', { 'class': 'spinning' }, _('Saving configuration data…')) ]); - L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal); + Promise.all(Object.keys(data).map(function(file) { + return fs.write(file, data[file]).catch(function(err) { + ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(file, err) ])); + }); + })).then(ui.hideModal); } }, _('Save')), ])); - L.showModal(_('OPKG Configuration'), body); + ui.showModal(_('OPKG Configuration'), body); }); } @@ -715,7 +831,7 @@ function handleRemove(ev) ]); } - L.showModal(_('Remove package %h').format(pkg.name), [ + ui.showModal(_('Remove package %h').format(pkg.name), [ E('ul', {}, [ E('li', '%s: %h'.format(_('Version'), pkg.version)), E('li', '%s: %h'.format(_('Size'), size)) @@ -729,7 +845,7 @@ function handleRemove(ev) E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [ E('div', { 'class': 'btn', - 'click': L.hideModal + 'click': ui.hideModal }, _('Cancel')), ' ', E('div', { @@ -749,15 +865,27 @@ function handleOpkg(ev) var cmd = ev.target.getAttribute('data-command'), pkg = ev.target.getAttribute('data-package'), rem = document.querySelector('input[name="autoremove"]'), - owr = document.querySelector('input[name="overwrite"]'), - url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd); + owr = document.querySelector('input[name="overwrite"]'); - var dlg = L.showModal(_('Executing package manager'), [ + var dlg = ui.showModal(_('Executing package manager'), [ E('p', { 'class': 'spinning' }, _('Waiting for the opkg %h command to complete…').format(cmd)) ]); - L.post(url, { package: pkg, autoremove: rem ? rem.checked : false, overwrite: owr ? owr.checked : false }, function(xhr, res) { + var argv = [ '--force-removal-of-dependent-packages' ]; + + if (rem && rem.checked) + argv.push('--autoremove'); + + if (owr && owr.checked) + argv.push('--force-overwrite'); + + argv.push(cmd); + + if (pkg != null) + argv.push(pkg); + + fs.exec('/bin/opkg', argv).then(function(res) { dlg.removeChild(dlg.lastChild); if (res.stdout) @@ -775,7 +903,7 @@ function handleOpkg(ev) E('div', { 'class': 'btn', 'click': L.bind(function(res) { - L.hideModal(); + ui.hideModal(); updateLists(); if (res.code !== 0) @@ -784,6 +912,9 @@ function handleOpkg(ev) resolveFn(res); }, this, res) }, _('Dismiss')))); + }).catch(function(err) { + ui.addNotification(null, E('p', _('Unable to execute opkg %s command: %s').format(cmd, err))); + ui.hideModal(); }); }); } @@ -791,8 +922,8 @@ function handleOpkg(ev) function handleUpload(ev) { var path = '/tmp/upload.ipk'; - return L.ui.uploadFile(path).then(L.bind(function(btn, res) { - L.showModal(_('Manually install package'), [ + return ui.uploadFile(path).then(L.bind(function(btn, res) { + ui.showModal(_('Manually install package'), [ E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install %h?').format(res.name)), E('ul', {}, [ res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '', @@ -802,8 +933,8 @@ function handleUpload(ev) E('div', { 'class': 'right' }, [ E('div', { 'click': function(ev) { - L.hideModal(); - L.fs.remove(path); + ui.hideModal(); + fs.remove(path); }, 'class': 'btn cbi-button-neutral' }, _('Cancel')), ' ', @@ -813,7 +944,7 @@ function handleUpload(ev) 'data-package': path, 'click': function(ev) { handleOpkg(ev).finally(function() { - L.fs.remove(path) + fs.remove(path) }); } }, _('Install')) @@ -822,7 +953,16 @@ function handleUpload(ev) }, this, ev.target)); } -function updateLists() +function downloadLists() +{ + return Promise.all([ + callMountPoints(), + fs.exec_direct('/usr/libexec/opkg-list', [ 'available' ]), + fs.exec_direct('/usr/libexec/opkg-list', [ 'installed' ]) + ]); +} + +function updateLists(data) { cbi_update_table('#packages', [], E('div', { 'class': 'spinning' }, _('Loading package information…'))); @@ -830,42 +970,104 @@ function updateLists() packages.available = { providers: {}, pkgs: {} }; packages.installed = { providers: {}, pkgs: {} }; - L.get('admin/system/opkg/statvfs', null, function(xhr, stat) { + return (data ? Promise.resolve(data) : downloadLists()).then(function(data) { var pg = document.querySelector('.cbi-progressbar'), - total = stat.blocks || 0, - free = stat.bfree || 0; + mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' })) + .sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 }; - pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%'; - pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0))); + pg.firstElementChild.style.width = Math.floor(mount.size ? ((100 / mount.size) * mount.free) : 100) + '%'; + pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, mount.free)); - L.get('admin/system/opkg/list/available', null, function(xhr) { - parseList(xhr.responseText, packages.available); - L.get('admin/system/opkg/list/installed', null, function(xhr) { - parseList(xhr.responseText, packages.installed); - display(document.querySelector('input[name="filter"]').value); - }); - }); + parseList(data[1], packages.available); + parseList(data[2], packages.installed); + + display(document.querySelector('input[name="filter"]').value); }); } -window.requestAnimationFrame(function() { - var filter = document.querySelector('input[name="filter"]'), - keyTimeout = null; +var keyTimeout = null; + +function handleKeyUp(ev) { + if (keyTimeout !== null) + window.clearTimeout(keyTimeout); + + keyTimeout = window.setTimeout(function() { + display(ev.target.value); + }, 250); +} + +return L.view.extend({ + load: function() { + return downloadLists(); + }, + + render: function(listData) { + var query = decodeURIComponent(L.toArray(location.search.match(/\bquery=([^=]+)\b/))[1] || ''); + + var view = E([], [ + E('style', { 'type': 'text/css' }, [ css ]), + + E('h2', {}, _('Software')), + + E('div', { 'class': 'controls' }, [ + E('div', {}, [ + E('label', {}, _('Free space') + ':'), + E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ])) + ]), + + E('div', {}, [ + E('label', {}, _('Filter') + ':'), + E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'keyup': handleKeyUp }), + E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ]) + ]), + + E('div', {}, [ + E('label', {}, _('Download and install package') + ':'), + E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) } }), + E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall }, [ _('OK') ]) + ]), + + E('div', {}, [ + E('label', {}, _('Actions') + ':'), ' ', + E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handleOpkg }, [ _('Update lists…') ]), '\u00a0', + E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload }, [ _('Upload Package…') ]), '\u00a0', + E('button', { 'class': 'btn cbi-button-neutral', 'click': handleConfig }, [ _('Configure opkg…') ]) + ]) + ]), + + E('ul', { 'class': 'cbi-tabmenu mode' }, [ + E('li', { 'data-mode': 'available', 'class': 'available cbi-tab', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Available') ])), + E('li', { 'data-mode': 'installed', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Installed') ])), + E('li', { 'data-mode': 'updates', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Updates') ])) + ]), - filter.value = filter.getAttribute('value'); - filter.addEventListener('keyup', - function(ev) { - if (keyTimeout !== null) - window.clearTimeout(keyTimeout); + E('div', { 'class': 'controls', 'style': 'display:none' }, [ + E('div', { 'id': 'pager', 'class': 'center' }, [ + E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage }, [ '«' ]), + E('div', { 'class': 'text' }, [ 'dummy' ]), + E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage }, [ '»' ]) + ]) + ]), + + E('div', { 'id': 'packages', 'class': 'table' }, [ + E('div', { 'class': 'tr cbi-section-table-titles' }, [ + E('div', { 'class': 'th col-2 left' }, [ _('Package name') ]), + E('div', { 'class': 'th col-2 left version' }, [ _('Version') ]), + E('div', { 'class': 'th col-1 center size'}, [ _('Size (.ipk)') ]), + E('div', { 'class': 'th col-10 left' }, [ _('Description') ]), + E('div', { 'class': 'th right' }, [ '\u00a0' ]) + ]) + ]) + ]); - keyTimeout = window.setTimeout(function() { - display(ev.target.value); - }, 250); + requestAnimationFrame(function() { + updateLists(listData) }); - document.querySelector('#pager > .prev').addEventListener('click', handlePage); - document.querySelector('#pager > .next').addEventListener('click', handlePage); - document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode); + return view; + }, - updateLists(); + handleSave: null, + handleSaveApply: null, + handleReset: null }); -- cgit v1.2.3