diff options
-rw-r--r-- | guix-web/view.scm | 4 | ||||
-rw-r--r-- | js/controller/packages.js | 124 | ||||
-rw-r--r-- | js/model/packages.js | 49 | ||||
-rw-r--r-- | js/packages.js | 387 | ||||
-rw-r--r-- | js/routes.js | 2 | ||||
-rw-r--r-- | js/view/packages.js | 251 |
6 files changed, 428 insertions, 389 deletions
diff --git a/guix-web/view.scm b/guix-web/view.scm index e134564..f444fbb 100644 --- a/guix-web/view.scm +++ b/guix-web/view.scm @@ -50,7 +50,9 @@ (javascript "/js/lib/mithril.js" expat) (javascript "/js/utils.js" agpl3+) (javascript "/js/view/layout.js" agpl3+) - (javascript "/js/packages.js" agpl3+) + (javascript "/js/model/packages.js" agpl3+) + (javascript "/js/view/packages.js" agpl3+) + (javascript "/js/controller/packages.js" agpl3+) (javascript "/js/routes.js" agpl3+))) (define (script-tag javascript) diff --git a/js/controller/packages.js b/js/controller/packages.js new file mode 100644 index 0000000..3a7858d --- /dev/null +++ b/js/controller/packages.js @@ -0,0 +1,124 @@ +// 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/>. + +(function(packages) { + packages.controller = (function() { + function controller() { + var self = this; + + this.packages = packages.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 packages.Sorter("name")); + this.phase = m.prop(packages.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 packages.Sorter(field)); + } + + this.doSearch(); + }; + + controller.prototype.installSelectedPackage = function() { + var self = this; + + this.phase(packages.PHASE_DERIVATION); + + m.request({ + method: "POST", + url: "/packages/" + .concat(this.selectedPackage().name) + .concat("/install") + }).then(function() { + self.phase(packages.PHASE_SUCCESS); + }, function() { + self.phase(packages.PHASE_ERROR); + }); + }; + + return controller; + })(); +})(guix.packages); diff --git a/js/model/packages.js b/js/model/packages.js new file mode 100644 index 0000000..5ea76a4 --- /dev/null +++ b/js/model/packages.js @@ -0,0 +1,49 @@ +// 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/>. + +guix.packages = {}; + +(function(packages) { + packages.Packages = function() { + return m.request({ method: "GET", url: "packages.json" }); + }; + + packages.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 packages.Sorter(this.field, !this.isDescending); + }; + + return Sorter; + })(); + + packages.PHASE_NONE = 0; + packages.PHASE_PROMPT = 1; + packages.PHASE_DERIVATION = 2; + packages.PHASE_SUCCESS = 3; + packages.PHASE_ERROR = 4; +})(guix.packages); diff --git a/js/packages.js b/js/packages.js deleted file mode 100644 index 3ec473a..0000000 --- a/js/packages.js +++ /dev/null @@ -1,387 +0,0 @@ -// 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() - ]); -}; diff --git a/js/routes.js b/js/routes.js index baa03f8..d749d3c 100644 --- a/js/routes.js +++ b/js/routes.js @@ -16,5 +16,5 @@ // <http://www.gnu.org/licenses/>. m.route(document.body, "/", { - "/": guix + "/": guix.packages }); diff --git a/js/view/packages.js b/js/view/packages.js new file mode 100644 index 0000000..e356984 --- /dev/null +++ b/js/view/packages.js @@ -0,0 +1,251 @@ +// 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/>. + +(function(packages) { + packages.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(packages.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 packages.PHASE_PROMPT: + return [ + m("p", "Do you want to install the following packages?"), + m("ul", [ + m("li", [ + ctrl.selectedPackage().name, + " ", + ctrl.selectedPackage().version + ]) + ]) + ]; + case packages.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 packages.PHASE_SUCCESS: + return m(".alert.alert-success", "Installation complete!"); + case packages.PHASE_ERROR: + return m(".alert.alert-danger", "Installation failed!"); + } + + return null; + } + + function renderButtons() { + switch(ctrl.phase()) { + case packages.PHASE_PROMPT: + return [ + m(".btn.btn-default", "Cancel"), + m(".btn.btn-primary", { + onclick: function() { + ctrl.installSelectedPackage(); + m.redraw(); + } + }, "Install"), + ]; + case packages.PHASE_DERIVATION: + return m(".btn.btn-danger", "Abort"); + case packages.PHASE_SUCCESS: + case packages.PHASE_ERROR: + return m(".btn.btn-primary", { + onclick: function() { + ctrl.phase(packages.PHASE_NONE); + } + }, "Close"); + } + + return null; + } + + if(ctrl.phase() != packages.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() + ]); + }; +})(guix.packages); |