From 293294f2401f3dac6d07e9af65b5582ba893a777 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 9 Feb 2015 08:34:37 -0500 Subject: js: Overhaul UI with FRP! * css/guix.css: New loading spinner. * js/lib/kefir.js: New file. * js/model/packages.js (guix.packages.Packages): Cache result. (guix.packages.Sorter, guix.packages.Pager): Delete. (guix.packages.installPackage): New function. * js/utils.js (K): New variable. (guix.withEmit, guix.withEmitAttr, guix.makeModule): New functions. * js/view/ui.js (guix.ui.paginate, guix.ui.spinUntil): New functions. (guix.ui.spinner): New variable. * js/controller/generations.js: Rewrite. * js/controller/packageInfo.js: Rewrite * js/controller/packages.js: Rewrite. * js/view/packages.js: Rewrite. * js/view/generations.js: Delete. * js/view/packageInfo.js: Delete. * js/routes.js: Use new modules. * guix/web/view/html.scm (javascripts): Update list. --- js/controller/generations.js | 49 +++++++-- js/controller/packageInfo.js | 59 ++++++++++- js/controller/packages.js | 246 ++++++++++++++++++++++++++++--------------- 3 files changed, 257 insertions(+), 97 deletions(-) (limited to 'js/controller') diff --git a/js/controller/generations.js b/js/controller/generations.js index 011dfa2..2b17a59 100644 --- a/js/controller/generations.js +++ b/js/controller/generations.js @@ -15,16 +15,43 @@ // License along with this program. If not, see // . -(function(generations) { - generations.controller = (function() { - function controller() { - this.generations = m.prop([]); +guix.generations.controller = function() { + var generations = K.fromPromise(guix.generations.Generations()); - generations.Generations() - .then(this.generations) - .then(m.redraw); - } + return guix.ui.spinUntil(generations.map(function(generations) { + return [ + guix.ui.headerWithBadge("Generations", generations.length), + m("table.table.table-bordered", [ + m("thead", m("tr", [ + m("th", "#"), + m("th", "Name"), + m("th", "Version"), + m("th", "Output"), + m("th", "Location") + ])), + m("tbody", [ + generations.map(function(generation) { + var entries = generation.manifestEntries; - return controller; - })(); -})(guix.generations); + function renderRow(entry, isFirst) { + return m("tr", [ + isFirst ? m("td", { + rowspan: entries.length + }, m("strong", generation.number)) : null, + m("td", entry.name), + m("td", entry.version), + m("td", entry.output), + m("td", entry.location) + ]); + } + + return [renderRow(entries[0], true)] + .concat(entries.slice(1).map(function (entry) { + return renderRow(entry, false); + })); + }) + ]) + ]) + ]; + })).map(guix.withLayout); +}; diff --git a/js/controller/packageInfo.js b/js/controller/packageInfo.js index ebd6a51..bb6c0b4 100644 --- a/js/controller/packageInfo.js +++ b/js/controller/packageInfo.js @@ -16,10 +16,61 @@ // . (function() { - var packageInfo = guix.packageInfo = {}; + guix.packageInfo = {}; - packageInfo.controller = function() { - this.name = m.route.param("name"); - this.packages = guix.packages.PackagesByName(this.name); + guix.packageInfo.controller = function() { + var name = m.route.param("name"); + var packages = K.fromPromise(guix.packages.PackagesByName(name)); + + // View. + return guix.ui.spinUntil(packages.map(function(packages) { + var packageCount = (function() { + var count = packages.length; + var units = count > 1 ? " versions" : " version"; + + return count.toString().concat(units); + })(); + + function describeInputs(inputs, description) { + return _.isEmpty(inputs) ? [] : [ + m("dt", description), + m("dd", m("ul", inputs.map(function(p) { + return m("li", m("a", { + config: m.route, + href: "/package/".concat(p.name) + }, p.name.concat(" ").concat(p.version))); + }))) + ]; + } + + function describePackage(package) { + var baseDescription = [ + m("dt", "Version"), + m("dd", package.version), + m("dt", "Synopsis"), + m("dd", package.synopsis), + m("dt", "Description"), + m("dd", package.description), + m("dt", "License"), + m("dd", guix.ui.licenseList(package)) + ]; + var inputs = describeInputs(package.inputs, "Inputs"); + var nativeInputs = describeInputs(package.nativeInputs, + "Native Inputs"); + var propagatedInputs = describeInputs(package.propagatedInputs, + "Propagated Inputs"); + return m("li", m("dl", _.flatten([ + baseDescription, + inputs, + nativeInputs, + propagatedInputs + ], true))); + } + + return [ + guix.ui.headerWithBadge(name, packageCount), + m("ul.list-unstyled", packages.map(describePackage)) + ]; + })).map(guix.withLayout); }; })(); diff --git a/js/controller/packages.js b/js/controller/packages.js index 9cac565..6a3ad75 100644 --- a/js/controller/packages.js +++ b/js/controller/packages.js @@ -18,99 +18,181 @@ (function() { var packages = guix.packages; - packages.controller = (function() { + packages.controller = function() { var PAGE_SIZE = 20; - - function Pager(items) { - return new packages.Pager(items, PAGE_SIZE); - } - - function controller() { - var self = this; - - 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.pager = m.prop(Pager([])); - this.phase = m.prop(packages.PHASE_NONE); - this.selectedPackage = m.prop(null); - this.packages = m.prop([]); - - packages.Packages() - .then(this.packages) - .then(function(packages) { - // All packages are visible initially - self.sortAndPage(packages); - }) - .then(m.redraw); - }; - - controller.prototype.packageCount = function() { - return _.chain(this.pager().pages) - .pluck('length') - .reduce(guix.add, 0) - .value(); - }; - - controller.prototype.sortAndPage = function(packages) { - this.pager(Pager(this.sorter().sort(packages))); - }; - - controller.prototype.doSearch = function() { - var regexp = new RegExp(this.searchTerm(), "i"); - var filteredPackages = this.packages().filter(function(package) { + // Throttle search to twice per second maximum. + var SEARCH_THROTTLE = 500; + + var packages = K.fromPromise(guix.packages.Packages()); + var searchTerm = K.emitter(); + var sorterStream = K.emitter(); + var pageIndex = K.emitter(); + var phaseStream = K.emitter(); + var selectedPackageStream = K.emitter(); + + var filteredPackages = K.combine([ + packages, + searchTerm.throttle(SEARCH_THROTTLE).toProperty("") + ], function(packages, search) { + var regexp = new RegExp(search === "" ? ".*" : search, "i"); + + return packages.filter(function(package) { return regexp.test(package.name) || regexp.test(package.synopsis); }); + }); + + var sorter = sorterStream.toProperty({ + field: "name", + reverse: false + }); + + var sortFields = { + license: 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); + }, ""); + } - this.sortAndPage(filteredPackages); + return package.license.name; + } }; - 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)); + var sortedPackages = K.combine([ + filteredPackages, + sorter + ], function(packages, sorter) { + var field = sorter.field; + var sorted = _.sortBy(packages, sortFields[field] || field); + + return sorter.reverse ? sorted.reverse() : sorted; + }); + + var pages = sortedPackages.map(function(packages) { + return guix.chunk(packages, PAGE_SIZE); + }).toProperty([[]]); + + var page = K.combine([ + pages, + pageIndex.toProperty(0) + ], function(pages, i) { + return { + index: i, + packages: pages[i] + }; + }); + + var phase = phaseStream.toProperty(guix.packages.PHASE_NONE); + + return guix.ui.spinUntil(K.combine([ + packages, + pages, + page, + searchTerm.toProperty(""), + sorter, + phase, + selectedPackageStream.toProperty(null) + ], function(packages, pages, page, search, sorter, phase, selectedPackage) { + function renderName(package) { + var name = package.name; + + return m("a", { + config: m.route, + href: "/package/".concat(name) + }, name); } - this.doSearch(); - }; + function renderHomepage(package) { + if(package.homepage) { + return m("a", { href: package.homepage }, package.homepage); + } else { + return ""; + } + } - controller.prototype.installSelectedPackage = function() { - var self = this; + function renderHeader(title, field) { + var isCurrentSorter = sorter.field === field; + var sorterClass = (function() { + if(isCurrentSorter) { + return sorter.reverse ? + "sorter sort-descend" : + "sorter sort-ascend"; + } - this.phase(packages.PHASE_DERIVATION); + return "sorter"; + })(); + + return m("th", { + class: sorterClass, + onclick: function() { + if(isCurrentSorter) { + sorterStream.emit({ + field: field, + reverse: !sorter.reverse + }); + } else { + sorterStream.emit({ + field: field, + reverse: false + }); + } + } + }, title); + } - 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); - }); - }; + function renderInstallLink(package) { + return m("a", { + href: "#", + onclick: function() { + selectedPackageStream.emit(package); + phaseStream.emit(guix.packages.PHASE_PROMPT); + } + }, "install"); + } - return controller; - })(); + var pagination = guix.ui.paginate(page.index, pages.length, + 10, pageIndex); + + return [ + guix.ui.headerWithBadge("Packages", packages.length), + // Installation modal + guix.packages.view.installModal(selectedPackage, phase, phaseStream), + // Search box + m("input.form-control", { + type: "text", + placeholder: "Search", + oninput: guix.withEmitAttr("value", searchTerm) + }), + pagination, + // Package table + m("table.table", [ + m("thead", [ + m("tr", [ + renderHeader("Name", "name"), + renderHeader("Version", "version"), + renderHeader("Synopsis", "synopsis"), + renderHeader("Home page", "homepage"), + renderHeader("License", "license"), + m("th", "") + ]) + ]), + m("tbody", [ + page.packages.map(function(package) { + return m("tr", [ + m("td", renderName(package)), + m("td", package.version), + m("td", package.synopsis), + m("td", renderHomepage(package)), + m("td", guix.ui.licenseList(package)), + m("td", renderInstallLink(package)) + ]); + }) + ]) + ]), + pagination + ]; + })).map(guix.withLayout); + }; })(); -- cgit v1.2.3