diff options
Diffstat (limited to 'js/packages.js')
-rw-r--r-- | js/packages.js | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/js/packages.js b/js/packages.js new file mode 100644 index 0000000..3ec473a --- /dev/null +++ b/js/packages.js @@ -0,0 +1,387 @@ +// guix-web - Web interface for GNU Guix +// Copyright © 2014 David Thompson <davet@gnu.org> +// +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License +// as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// <http://www.gnu.org/licenses/>. + +var guix = guix || {}; + +guix.Packages = function() { + return m.request({ method: "GET", url: "packages.json" }); +}; + +guix.Sorter = (function() { + function Sorter(field, isDescending) { + this.field = field; + this.isDescending = _.isUndefined(isDescending) ? false : isDescending; + }; + + Sorter.prototype.sort = function(array) { + var result = _.sortBy(array, this.field); + + return this.isDescending ? result.reverse() : result; + }; + + Sorter.prototype.reverse = function() { + return new guix.Sorter(this.field, !this.isDescending); + }; + + return Sorter; +})(); + +guix.PHASE_NONE = 0; +guix.PHASE_PROMPT = 1; +guix.PHASE_DERIVATION = 2; +guix.PHASE_SUCCESS = 3; +guix.PHASE_ERROR = 4; + +guix.controller = (function() { + function controller() { + var self = this; + + this.packages = guix.Packages(); + this.pages = m.prop([]); + this.currentPageIndex = 0; + this.pageSize = 20; + this.searchTerm = m.prop(""); + this.columns = [ + { header: "Name", sortField: "name" }, + { header: "Version", sortField: "version" }, + { header: "Synopsis", sortField: "synopsis" }, + { header: "Home Page", sortField: "homepage" }, { + header: "License", + sortField: function(package) { + if(_.isArray(package.license)) { + // Concatenate all license names together for sorting. + return package.license.reduce(function(memo, l) { + return memo.concat(l.name); + }, ""); + } + + return package.license.name; + } + } + ]; + this.sorter = m.prop(new guix.Sorter("name")); + this.phase = m.prop(guix.PHASE_NONE); + this.selectedPackage = m.prop(null); + + // All packages are visible initially + this.packages.then(function(packages) { + self.pages(self.paginate(packages, self.pageSize)); + }); + }; + + + controller.prototype.paginate = function(array, pageSize) { + return guix.chunk(this.sorter().sort(array), pageSize); + }; + + controller.prototype.currentPage = function() { + return this.pages()[this.currentPageIndex] || []; + }; + + controller.prototype.isFirstPage = function() { + return this.currentPageIndex === 0; + }; + + controller.prototype.isLastPage = function() { + return this.currentPageIndex === this.pages().length - 1; + }; + + controller.prototype.isCurrentPage = function(i) { + return this.currentPageIndex === i; + }; + + controller.prototype.packageCount = function() { + return this.pages().reduce(function(memo, page) { + return memo + page.length; + }, 0); + }; + + controller.prototype.doSearch = function() { + var regexp = new RegExp(this.searchTerm(), "i"); + + this.pages(this.paginate(this.packages().filter(function(package) { + return regexp.test(package.name) || + regexp.test(package.synopsis); + }), this.pageSize)); + // Reset pagination + this.currentPageIndex = 0; + }; + + controller.prototype.sortBy = function(field) { + if(this.sorter().field === field) { + // Reverse sort order if the field is the same as before. + this.sorter(this.sorter().reverse()); + } else { + this.sorter(new guix.Sorter(field)); + } + + this.doSearch(); + }; + + controller.prototype.installSelectedPackage = function() { + var self = this; + + this.phase(guix.PHASE_DERIVATION); + + m.request({ + method: "POST", + url: "/packages/" + .concat(this.selectedPackage().name) + .concat("/install") + }).then(function() { + self.phase(guix.PHASE_SUCCESS); + }, function() { + self.phase(guix.PHASE_ERROR); + }); + }; + + return controller; +})(); + + +guix.view = function(ctrl) { + function renderName(package) { + var name = package.name; + + return m("a", { href: "/packages/".concat(name) }, name); + } + + function renderHomepage(package) { + if(package.homepage) { + return m("a", { href: package.homepage }, package.homepage); + } else { + return ""; + } + } + + function renderLicense(package) { + function licenseLink(license) { + return m("a", { href: license.uri }, license.name); + } + + if(_.isArray(package.license)) { + return m("ul.list-inline", package.license.map(function(license) { + return m("li", licenseLink(license)); + })); + } else if(package.license) { + return licenseLink(package.license); + } else { + return ""; + } + } + + function renderInstallLink(package) { + return m("a", { + href: "#", + onclick: function() { + ctrl.selectedPackage(package); + ctrl.phase(guix.PHASE_PROMPT); + return false; + } + }, "install"); + } + + function renderPackageTable() { + return m("table.table", [ + m("thead", [ + m("tr", [ + ctrl.columns.map(function(column) { + return m("th", { + class: columnHeaderClass(column), + onclick: function() { + ctrl.sortBy(column.sortField); + } + }, column.header); + }).concat([m("th", "")]) + ]) + ]), + m("tbody", [ + ctrl.currentPage().map(function(package) { + return m("tr", [ + m("td", renderName(package)), + m("td", package.version), + m("td", package.synopsis), + m("td", renderHomepage(package)), + m("td", renderLicense(package)), + m("td", renderInstallLink(package)) + ]); + }) + ]) + ]); + } + + function renderPagination() { + function renderPage(text, opts) { + return m("li", { + class: opts.class || "", + onclick: opts.onclick + }, m("a", { href: "#" }, text)); + } + + return m("ul.pagination", [ + // Back page + renderPage("«", { + class: ctrl.isFirstPage() ? "disabled" : "", + onclick: function() { + ctrl.currentPageIndex--; + + return false; + } + }) + ].concat(ctrl.pages().map(function(page, i) { + // Jump to page + return renderPage(i + 1, { + class: ctrl.isCurrentPage(i) ? "active" : "", + onclick: function() { + ctrl.currentPageIndex = i; + + return false; + } + }); + })).concat([ + // Forward page + renderPage("»", { + class: ctrl.isLastPage() ? "disabled" : "", + onclick: function() { + ctrl.currentPageIndex++; + + return false; + } + }) + ])); + } + + function renderSearchBox() { + return m("input.form-control", { + type: "text", + placeholder: "Search", + onchange: m.withAttr("value", function(value) { + ctrl.searchTerm(value); + ctrl.doSearch(); + }), + value: ctrl.searchTerm() + }); + } + + function columnHeaderClass(column) { + var sorter = ctrl.sorter(); + + if(column.sortField === sorter.field) { + return sorter.isDescending ? "sorter sort-descend" : "sorter sort-ascend"; + } + + return "sorter"; + } + + function renderModal() { + function renderBody() { + switch(ctrl.phase()) { + case guix.PHASE_PROMPT: + return [ + m("p", "Do you want to install the following packages?"), + m("ul", [ + m("li", [ + ctrl.selectedPackage().name, + " ", + ctrl.selectedPackage().version + ]) + ]) + ]; + case guix.PHASE_DERIVATION: + return [ + m("p", [ + "Installing ", + ctrl.selectedPackage().name, + " ", + ctrl.selectedPackage().version, + "..." + ]), + m(".progress", [ + m(".progress-bar.progress-bar-striped.active", { + role: "progressbar", + style: { width: "100%" } + }) + ]) + ]; + case guix.PHASE_SUCCESS: + return m(".alert.alert-success", "Installation complete!"); + case guix.PHASE_ERROR: + return m(".alert.alert-danger", "Installation failed!"); + } + + return null; + } + + function renderButtons() { + switch(ctrl.phase()) { + case guix.PHASE_PROMPT: + return [ + m(".btn.btn-default", "Cancel"), + m(".btn.btn-primary", { + onclick: function() { + ctrl.installSelectedPackage(); + m.redraw(); + } + }, "Install"), + ]; + case guix.PHASE_DERIVATION: + return m(".btn.btn-danger", "Abort"); + case guix.PHASE_SUCCESS: + case guix.PHASE_ERROR: + return m(".btn.btn-primary", { + onclick: function() { + ctrl.phase(guix.PHASE_NONE); + } + }, "Close"); + } + + return null; + } + + if(ctrl.phase() != guix.PHASE_NONE) { + return [ + m(".modal-backdrop.in"), + m("div.modal.modal-open", { + style: { + display: "block" + } + }, m(".modal-dialog", [ + m(".modal-content", [ + m(".modal-header", [ + m("h4.modal-title", "Install Packages") + ]), + m(".modal-body", renderBody()), + m(".modal-footer", renderButtons()) + ]) + ])) + ]; + } + + return null; + } + + return guix.withLayout([ + m("h2", [ + "Packages", + m("span.badge", ctrl.packageCount()) + ]), + renderModal(), + renderSearchBox(), + renderPackageTable(), + renderPagination() + ]); +}; |