// guix-web - Web interface for GNU Guix // Copyright © 2014 David Thompson // // 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 // . var guix = {}; guix.chunk = function(array, size) { return array.reduce(function(memo, value, i) { var currentSlice = _(memo).last(); if(i / size < memo.length) { currentSlice.push(value); } else { memo.push([value]); } return memo; }, []); }; 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 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 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-default.disabled", "Please wait..."); 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 [ m("h2", [ "Packages", m("span.badge", ctrl.packageCount()) ]), renderModal(), m("input.form-control", { type: "text", placeholder: "Search", onchange: m.withAttr("value", function(value) { ctrl.searchTerm(value); ctrl.doSearch(); }), value: ctrl.searchTerm() }), 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)) ]); }) ]) ]), renderPagination() ]; }; m.module(document.getElementById("guix"), guix);