From 2ef3cc9b722da0d0b61966871ba0a0b24e0d7739 Mon Sep 17 00:00:00 2001 From: tim-zh Date: Sun, 13 Nov 2016 22:20:06 +0300 Subject: example browser --- tools/gui/resources/web/definitions.js | 223 +++++++++++++++++++++++++++++++++ tools/gui/resources/web/index.html | 52 +++++--- tools/gui/resources/web/main.js | 150 +++++++--------------- tools/gui/resources/web/styles.css | 75 ++++++++++- tools/gui/src/JettyServer.scala | 4 +- tools/gui/src/Main.scala | 63 +++++++++- 6 files changed, 441 insertions(+), 126 deletions(-) create mode 100644 tools/gui/resources/web/definitions.js (limited to 'tools') diff --git a/tools/gui/resources/web/definitions.js b/tools/gui/resources/web/definitions.js new file mode 100644 index 0000000..d1313d4 --- /dev/null +++ b/tools/gui/resources/web/definitions.js @@ -0,0 +1,223 @@ +function ajax(url, data, method) { + let f = method == "post" ? $.post : $.get; + return f("/api" + url, data).fail(e => + Notifications.showFail(e) + ); +} + +function getFlags() { + return "readme/" + $("#readme-flag")[0].checked + + " dotty/" + $("#dotty-flag")[0].checked + + " uberJar/" + $("#uberJar-flag")[0].checked + + " wartremover/" + $("#wartremover-flag")[0].checked; +} + +let ProjectLocation = { + _base: "", + + init: function (container, nameInput) { + this._container = container; + nameInput.keyup(function () { + ProjectLocation.updateName(this.value); + }); + ajax("/cwd").done(path => { + ProjectLocation._container.append(path[0]); + ProjectLocation._base = ProjectLocation._container.html(); + }); + }, + + updateName: function (x) { + this._container.html(this._base + (x ? "/" + x : "")); + } +}; + +let Popup = { + init: function (container, table) { + this._container = container; + this._table = table; + document.body.onkeydown = function (event) { + if (event.keyCode == 27) + Popup.hide(); + }; + }, + + show: function (contents) { + this._container.show(); + this._table.html(contents); + }, + + hide: function () { + this._container.hide(); + this._table.html(""); + } +}; + +let Notifications = { + init: function () { + Notification.requestPermission(); + }, + + show: function (text, title) { + new Notification(title || "", {body: text}); + }, + + showFail: function (e) { + let head = e.status == 0 ? "Error" : e.status + " " + e.statusText; + let body = e.status == 0 ? "No response from UI server." : e.responseText; + this.show(body, head); + } +}; + +let Dependencies = { + _list: [], + + init: function (searchBtn, queryInput, dependenciesNode) { + this._queryInput = queryInput; + queryInput.keyup(function handleSearchInput(event) { + if (event.keyCode == 13) + Dependencies.search(); + }); + this._dependenciesNode = dependenciesNode; + }, + + serialize: function () { + return this._list.length == 0 ? "" : + this._list.map(l => l.group + "/" + l.artifact + "/" + l.version).reduce((a, b) => a + " " + b); + }, + + search: function () { + let query = this._queryInput.val(); + if (query) { + ajax("/dependency", {query: query}).done(data => { + let entries = data.response.docs.map(x => ({ + group: x.g, + artifact: x.a + })); + Popup.show(Dependencies._makeRowsFrom(entries, Dependencies.selectDependency)); + }); + document.activeElement.blur(); + } + }, + + selectDependency: function (selected) { + ajax("/dependency/version", {group: selected.group, artifact: selected.artifact}).done(data => { + let versions = data.response.docs.map(x => ({version: x.v})); + Popup.show(Dependencies._makeRowsFrom(versions, function (version) { + Dependencies.selectDependencyVersion(selected.group, selected.artifact, version.version); + })); + }); + }, + + selectDependencyVersion: function (group, artifact, version) { + let dependency = {group: group, artifact: artifact, version: version}; + this._list.push(dependency); + let scalaName = artifact.match(/^(.+)_\d+\.\d+$/); + let name = scalaName && scalaName[1] ? scalaName[1] : artifact; + let depDiv = $("
" + name + " " + version + "
"); + depDiv.click(function () { + Dependencies.removeDependency(dependency, depDiv); + }); + this._dependenciesNode.append(depDiv); + Popup.hide(); + }, + + removeDependency: function (d, div) { + this._list.splice(this._list.indexOf(d), 1); + div.remove(); + }, + + _makeRowsFrom: function (results, rowAction) { + if (results.length == 0) { + return []; + } else { + let rows = []; + let row = $(""); + rows.push(row); + let fields = []; + for (let field in results[0]) + if (results[0].hasOwnProperty(field)) { + fields.push(field); + $("" + field + "").appendTo(row); + } + results.forEach(result => { + let row = $(""); + row.click(function () { + rowAction(result); + }); + rows.push(row); + fields.forEach(field => $("" + result[field] + "").appendTo(row)); + }); + return rows; + } + } +}; + +let Examples = { + fetchExamples: () => { + let examplesContainer = $("#examples"); + examplesContainer.html(""); + ajax("/examples").done(data => { + data.forEach(name => { + var example = $(""); + example.click(() => Examples.selectExample(name)); + example.appendTo(examplesContainer); + }); + }); + }, + + selectExample: name => { + $("#examples-title").hide(); + let selectedExample = $("#selected-example"); + selectedExample.html(name); + selectedExample.show(); + ProjectLocation.updateName(name); + $("#examples").hide(); + $("#example-browser").show(); + $("#copy-project-btn").show(); + + Examples._fetchExampleFiles(name); + }, + + unselectExample: () => { + $("#examples-title").show(); + $("#selected-example").hide(); + ProjectLocation.updateName(""); + $("#examples").show(); + $("#example-browser").hide(); + $("#copy-project-btn").hide(); + $("#code-browser").hide(); + }, + + _fetchExampleFiles: name => { + let fileBrowser = $("#file-browser"); + fileBrowser.html(""); + ajax("/example/files", {name: name}).done(data => { + Examples._appendTree(data, fileBrowser, 0); + }); + }, + + selectedFileNode: null, + + _appendTree: function (node, parent, dirDepth) { + let div = $("
" + node.name + "
"); + div.appendTo(parent); + if (dirDepth % 2 == 0) + div.addClass("even-node"); + if (node.children) { + node.children.forEach(x => Examples._appendTree(x, div, dirDepth + 1)); + } else { + div.addClass("file-node"); + div.click(() => { + if (Examples.selectedFileNode) + Examples.selectedFileNode.removeClass("selected-node"); + Examples.selectedFileNode = div; + Examples.selectedFileNode.addClass("selected-node"); + ajax("/example/file", {path: node.path}).done(data => { + var codeBrowser = $("#code-browser"); + codeBrowser.show(); + codeBrowser.html(data); + }); + }); + } + } +}; diff --git a/tools/gui/resources/web/index.html b/tools/gui/resources/web/index.html index 9ad45a5..100c65b 100644 --- a/tools/gui/resources/web/index.html +++ b/tools/gui/resources/web/index.html @@ -10,33 +10,57 @@

CBT bootstrap

-
current dir:
-
-
+ + or +
-
- -
-
+
project location:
- - - - -
+
+
+
+
+ +
+ +
+
+ + + + + +
+ + +
- +
+ + select an example: + + +
+
+
+
+
+
+
+ +
+ diff --git a/tools/gui/resources/web/main.js b/tools/gui/resources/web/main.js index b4fe38b..139795a 100644 --- a/tools/gui/resources/web/main.js +++ b/tools/gui/resources/web/main.js @@ -1,129 +1,73 @@ -Notification.requestPermission(); +Notifications.init(); -$.get("/api/cwd").done(cwd => { - $("#cwd").append(cwd[0]); -}).fail(e => notifyFail(e)); +Popup.init($("#popup"), $("#popup-table")); -document.body.onkeydown = function (event) { - if (event.keyCode == 27) - hidePopup(); -}; +Dependencies.init($("#search-btn"), $("#query"), $("#dependencies")); -$("#create-project")[0].disabled = false; +ProjectLocation.init($("#cwd"), $("#name")); -let dependencies = []; +["#create-project-btn", "#copy-project-btn", "#flow-create-btn", "#flow-copy-btn"].forEach(id => $(id)[0].disabled = false); -function getFlags() { - return "readme/" + $("#readme-flag")[0].checked - + " dotty/" + $("#dotty-flag")[0].checked - + " uberJar/" + $("#uberJar-flag")[0].checked - + " wartremover/" + $("#wartremover-flag")[0].checked; +$("#flow-copy-btn")[0].style.width = $("#flow-create-btn")[0].offsetWidth + "px"; + +function setFlowCreate() { + $("#flow-create").show(); + $("#flow-copy").hide(); + + let createBtn = $("#flow-create-btn"); + let copyBtn = $("#flow-copy-btn"); + createBtn.blur(); + createBtn[0].disabled = true; + copyBtn[0].disabled = false; + + ProjectLocation.updateName($("#name").val()); } function createProject() { - let button = $("#create-project")[0]; + let button = $("#create-project-btn")[0]; let buttonText = button.innerHTML; button.innerHTML = "..."; button.blur(); button.disabled = true; - $.post("/api/project", { + ajax("/project/new", { name: $("#name").val(), pack: $("#package").val(), - dependencies: dependencies.length == 0 ? "" : - dependencies.map(l => l.group + "/" + l.artifact + "/" + l.version).reduce((a, b) => a + " " + b), + dependencies: Dependencies.serialize(), flags: getFlags() - }).done(() => { - notify("Done."); - }).fail(e => - notifyFail(e) - ).always(() => { + }, "post").done(() => { + Notifications.show("Done."); + }).always(() => { button.innerHTML = buttonText; button.disabled = false; }); } -function handleSearchInput(event) { - if (event.keyCode == 13) - search(); -} -function search() { - let query = $("#query").val(); - if (query) { - $.get("/api/dependency", { query: query }).done(data => { - let entries = data.response.docs.map(x => ({ - group: x.g, - artifact: x.a - })); - showPopup(makeRowsFrom(entries, selectDependency)); - }).fail(e => notifyFail(e)); - document.activeElement.blur(); - } -} -function selectDependency(selected) { - $.get("/api/dependency/version", { group: selected.group, artifact: selected.artifact }).done(data => { - let versions = data.response.docs.map(x => ({ version: x.v })); - $("#popup-table").html(makeRowsFrom(versions, function (version) { - selectDependencyVersion(selected.group, selected.artifact, version.version); - })); - }).fail(e => notifyFail(e)); -} -function selectDependencyVersion(group, artifact, version) { - let dependency = { group: group, artifact: artifact, version: version }; - dependencies.push(dependency); - let scalaName = artifact.match(/^(.+)_\d+\.\d+$/); - let name = scalaName && scalaName[1] ? scalaName[1] : artifact; - var depDiv = $("
" + name + " " + version + "
"); - depDiv.click(function () { - removeDependency(dependency, depDiv); - }); - $("#dependencies").append(depDiv); - hidePopup(); -} -function removeDependency(d, div) { - dependencies.splice(dependencies.indexOf(d), 1); - div.remove(); -} +function setFlowCopy() { + $("#flow-copy").show(); + $("#flow-create").hide(); -function showPopup(contents) { - $("#popup").show(); - $("#popup-table").html(contents); -} -function hidePopup() { - $("#popup").hide(); - $("#popup-table").html(""); -} + let createBtn = $("#flow-create-btn"); + let copyBtn = $("#flow-copy-btn"); + copyBtn.blur(); + copyBtn[0].disabled = true; + createBtn[0].disabled = false; -function makeRowsFrom(results, rowAction) { - if (results.length == 0) { - return []; - } else { - let rows = []; - let row = $(""); - rows.push(row); - let fields = []; - for (let field in results[0]) - if (results[0].hasOwnProperty(field)) { - fields.push(field); - $("" + field + "").appendTo(row); - } - results.forEach(result => { - let row = $(""); - row.click(function () { - rowAction(result); - }); - rows.push(row); - fields.forEach(field => $("" + result[field] + "").appendTo(row)); - }); - return rows; - } -} + Examples.unselectExample(); -function notify(text, title) { - new Notification(title || "", { body: text }); + Examples.fetchExamples(); } -function notifyFail(e) { - let head = e.status == 0 ? "Error" : e.status + " " + e.statusText; - let body = e.status == 0 ? "No response from UI server." : e.responseText; - notify(body, head); +function copyProject() { + let name = $("#selected-example").html(); + let button = $("#copy-project-btn")[0]; + let buttonText = button.innerHTML; + button.innerHTML = "..."; + button.blur(); + button.disabled = true; + ajax("/project/copy", {name: name}, "post").done(() => { + Notifications.show("Done."); + }).always(() => { + button.innerHTML = buttonText; + button.disabled = false; + }); } diff --git a/tools/gui/resources/web/styles.css b/tools/gui/resources/web/styles.css index 6aa45e9..759dd06 100644 --- a/tools/gui/resources/web/styles.css +++ b/tools/gui/resources/web/styles.css @@ -30,12 +30,16 @@ hr { border-bottom: transparent solid 1px; } +pre { + margin: 0 0 1em 0; +} + button, .small-btn { background: #dc322f; color: #fff; font-size: 1em; padding: 0.2em; - border: transparent solid 0.1em; + border: none; border-radius: 1em; text-transform: uppercase; cursor: pointer; @@ -47,9 +51,14 @@ button, .small-btn { padding: 0 1em; } +.link-btn:hover { + color: #dc322f; + cursor: pointer; +} + button:hover { - background: #073642; - border: #fff solid 0.1em; + background: #fff; + color: #002B36; } button:active { @@ -57,7 +66,8 @@ button:active { } button:disabled { - background: transparent; + background: #073642; + color: #fff; cursor: default; } @@ -66,6 +76,7 @@ input[type="text"] { color: #fff; border: none; width: 33%; + min-width: 15em; font-size: 1em; } @@ -115,6 +126,10 @@ input[type="checkbox"]:checked + label:after { border-color: #dc322f; } +.stroke { + text-shadow: 0 1px 0 #002B36, 0 -1px 0 #002B36, 1px 0 0 #002B36, -1px 0 0 #002B36; +} + .container { margin: 0 8%; text-align: center; @@ -138,10 +153,11 @@ button, .entry { left: 0; right: 0; margin: 0 auto; + border: #002B36 solid 0.5em; background: #073642; width: 84%; max-height: 80%; - overflow-y: scroll; + overflow-y: auto; text-align: center; display: none; z-index: 2; @@ -194,3 +210,52 @@ button, .entry { #popup tr:first-child:hover { color: #002B36; } + +#flow-create, #flow-copy { + display: none; +} + +#example-browser { + width: 100%; + float: left; +} + +#file-browser { + overflow-x: auto; + padding-right: 0.4em; + float: left; + max-width: 30%; +} + +#code-browser { + background: #073642; + font-size: 1.2rem; + text-align: left; + padding: 0.4em; + overflow-x: auto; + max-height: 100%; +} + +.browser-node { + font-size: 1.2rem; + text-align: left; + line-height: 1.2em; + background: #002B36; + padding: 0.2em 0 0 0.2em; +} + +.browser-node > div { + margin-left: 2em; +} + +.file-node { + cursor: pointer; +} + +.file-node:hover, .selected-node { + color: #dc322f; +} + +.even-node { + background: #073642; +} diff --git a/tools/gui/src/JettyServer.scala b/tools/gui/src/JettyServer.scala index d6024c2..a64c6bc 100644 --- a/tools/gui/src/JettyServer.scala +++ b/tools/gui/src/JettyServer.scala @@ -27,7 +27,7 @@ abstract class JettyServer(port: Int, staticFilesUrl: String) { response.setContentType("application/json") response.setCharacterEncoding("UTF-8") - route(request.getMethod, target, request.getParameter) match { + route(request.getMethod, target, request.getParameter, response.setContentType) match { case Success(result) => response.getWriter.write(result) case Failure(e: MalformedURLException) => @@ -63,5 +63,5 @@ abstract class JettyServer(port: Int, staticFilesUrl: String) { println("UI server stopped.") } - def route(method: String, path: String, param: String => String): Try[String] + def route(method: String, path: String, param: String => String, setContentType: String => Unit): Try[String] } diff --git a/tools/gui/src/Main.scala b/tools/gui/src/Main.scala index d7a9f7d..7bb299c 100644 --- a/tools/gui/src/Main.scala +++ b/tools/gui/src/Main.scala @@ -1,5 +1,7 @@ import java.io.{File, IOException} import java.net.MalformedURLException +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} import scala.io.Source import scala.util.{Failure, Success, Try} @@ -21,10 +23,13 @@ object Main { def launchUi(projectDirectory: File, scalaMajorVersion: String): Unit = { val staticBase = new File(cbt_home / "tools" / "gui" / "resources" / "web").toURI.toURL.toExternalForm val server = new JettyServer(uiPort, staticBase) { - override def route(method: String, path: String, param: String => String) = (method, path) match { + override def route(method: String, + path: String, + param: String => String, + setContentType: String => Unit) = (method, path) match { case ("GET", "/cwd") => Success(s"""["$projectDirectory"]""") - case ("POST", "/project") => + case ("POST", "/project/new") => val name = param("name") val defaultPackage = param("pack") val dependencies = param("dependencies") @@ -33,6 +38,14 @@ object Main { new ProjectBuilder(name, defaultPackage, dependencies, flags, projectDirectory, scalaMajorVersion).build() Success("[]") } + case ("POST", "/project/copy") => + val name = param("name") + val source = new File(cbt_home / "examples" / name) + val target = new File(projectDirectory.getAbsolutePath / name) + handleIoException { + new FileCopier(source, target).copy() + Success("[]") + } case ("GET", "/dependency") => val query = param("query") handleIoException(handleMavenBadResponse(searchDependency(query))) @@ -40,6 +53,28 @@ object Main { val group = param("group") val artifact = param("artifact") handleIoException(handleMavenBadResponse(searchDependencyVersion(group, artifact))) + case ("GET", "/examples") => + handleIoException { + val names = new File(cbt_home / "examples").listFiles().filter(_.isDirectory).sortBy(_.getName) + .map('"' + _.getName + '"').mkString(",") + Success(s"[$names]") + } + case ("GET", "/example/files") => + val name = param("name") + handleIoException { + val dir = new File(cbt_home / "examples" / name) + if (dir.exists()) + Success(serializeTree(dir)) + else + Failure(new IllegalArgumentException(s"Incorrect example name: $name")) + } + case ("GET", "/example/file") => + setContentType("text/plain") + val path = param("path") + handleIoException { + val file = new File(path) + Success(Source.fromFile(file).mkString) + } case _ => Failure(new MalformedURLException(s"Incorrect path: $path")) } @@ -82,4 +117,28 @@ object Main { Failure(new Exception(s"Bad response from $maven_host: $result")) } + private def serializeTree(file: File): String = { + val data = if (file.isDirectory) + s""","children":[${file.listFiles().sortBy(_.getName).map(serializeTree).mkString(",")}]""" + else + "" + s"""{"name":"${file.getName}","path":"${file.getAbsolutePath}"$data}""" + } + + private class FileCopier(source: File, target: File) extends SimpleFileVisitor[Path] { + + def copy() = Files.walkFileTree(source.toPath, this) + + override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes) = { + Files.createDirectories(target.toPath.resolve(source.toPath.relativize(dir))) + FileVisitResult.CONTINUE + } + + override def visitFile(file: Path, attrs: BasicFileAttributes) = { + Files.copy(file, target.toPath.resolve(source.toPath.relativize(file))) + FileVisitResult.CONTINUE + } + + } + } -- cgit v1.2.3