summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--guix-web/view.scm4
-rw-r--r--js/controller/packages.js124
-rw-r--r--js/model/packages.js49
-rw-r--r--js/packages.js387
-rw-r--r--js/routes.js2
-rw-r--r--js/view/packages.js251
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);