Switch to latest scalajs-react, split web module sources (#906)

This commit is contained in:
Alexandre Archambault 2018-09-26 12:09:29 +02:00 committed by GitHub
parent dd7c8c19d4
commit 528c2adc2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 665 additions and 586 deletions

View File

@ -153,6 +153,20 @@ To build a distributable binary
java -jar dist/coursier-cli.jar fetch --help
```
## Build the web demo
coursier is cross-compiled to scala-js, and can run in the browser. It has a [demo web site](https://coursier.github.io/coursier/#demo), that runs resolutions straight from your web browser.
Its sources are in the `web` module.
To build and test this demo site locally, you can do
```
$ sbt web/fastOptJS
$ open web/target/scala-2.12/classes/index.html
```
(on Linux, use `xdg-open` instead of `open`)
# Merging PRs on GitHub
Use either "Create merge commit" or "Squash and merge".

View File

@ -196,7 +196,7 @@ lazy val web = project
shared,
dontPublish,
libs ++= {
if (scalaBinaryVersion.value == "2.11")
if (scalaBinaryVersion.value == "2.12")
Seq(
CrossDeps.scalaJsJquery.value,
CrossDeps.scalaJsReact.value
@ -207,7 +207,7 @@ lazy val web = project
sourceDirectory := {
val dir = sourceDirectory.value
if (scalaBinaryVersion.value == "2.11")
if (scalaBinaryVersion.value == "2.12")
dir
else
dir / "target" / "dummy"
@ -222,7 +222,20 @@ lazy val web = project
WebDeps.react
.intransitive()
./("react-with-addons.js")
.minified("react-with-addons.min.js")
.commonJSName("React"),
WebDeps.react
.intransitive()
./("react-dom.js")
.minified("react-dom.min.js")
.dependsOn("react-with-addons.js")
.commonJSName("ReactDOM"),
WebDeps.react
.intransitive()
./("react-dom-server.js")
.minified("react-dom-server.min.js")
.dependsOn("react-dom.js")
.commonJSName("ReactDOMServer"),
WebDeps.bootstrapTreeView
.intransitive()
./("bootstrap-treeview.min.js")

View File

@ -98,18 +98,30 @@ package object compatibility {
// FIXME Won't work in the browser
lazy val cheerio = g.require("cheerio")
def listWebPageRawElements(page: String): Seq[String] = {
lazy val jqueryAvailable = !js.isUndefined(g.$)
val jquery = cheerio.load(page)
def listWebPageRawElements(page: String): Seq[String] = {
val links = new ListBuffer[String]
jquery("a").each({ self: js.Dynamic =>
val href = jquery(self).attr("href")
if (!js.isUndefined(href))
links += href.asInstanceOf[String]
()
}: js.ThisFunction0[js.Dynamic, Unit])
// getting weird "maybe a wrong Dynamic method signature" errors when trying to factor that more
if (jqueryAvailable)
g.$("<div></div>").html(page).find("a").each({ self: js.Dynamic =>
val href = g.$(self).attr("href")
if (!js.isUndefined(href))
links += href.asInstanceOf[String]
()
}: js.ThisFunction0[js.Dynamic, Unit])
else {
val jquery = cheerio.load(page)
jquery("a").each({ self: js.Dynamic =>
val href = jquery(self).attr("href")
if (!js.isUndefined(href))
links += href.asInstanceOf[String]
()
}: js.ThisFunction0[js.Dynamic, Unit])
}
links.result()
}

View File

@ -15,6 +15,6 @@ object CrossDeps {
def scalazCore = setting("org.scalaz" %%% "scalaz-core" % SharedVersions.scalaz)
def scalaJsDom = setting("org.scala-js" %%% "scalajs-dom" % "0.9.6")
def utest = setting("com.lihaoyi" %%% "utest" % "0.6.4")
def scalaJsJquery = setting("be.doeraene" %%% "scalajs-jquery" % "0.9.3")
def scalaJsReact = setting("com.github.japgolly.scalajs-react" %%% "core" % "0.9.0")
def scalaJsJquery = setting("be.doeraene" %%% "scalajs-jquery" % "0.9.4")
def scalaJsReact = setting("com.github.japgolly.scalajs-react" %%% "core" % "1.3.1")
}

View File

@ -4,7 +4,7 @@ import sbt.Keys._
object WebDeps {
def bootstrap = "org.webjars.bower" % "bootstrap" % "3.3.4"
def react = "org.webjars.bower" % "react" % "0.12.2"
def react = "org.webjars.bower" % "react" % "15.6.1"
def bootstrapTreeView = "org.webjars.bower" % "bootstrap-treeview" % "1.2.0"
def raphael = "org.webjars.bower" % "raphael" % "2.1.4"
}

View File

@ -0,0 +1,501 @@
package coursier.web
import coursier.{Dependency, MavenRepository, Module, Resolution}
import coursier.maven.MavenSource
import japgolly.scalajs.react.vdom.{Attr, TagMod}
import japgolly.scalajs.react.vdom.HtmlAttrs.dangerouslySetInnerHtml
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import scala.scalajs.js
import js.Dynamic.{global => g}
object App {
lazy val arbor = g.arbor
val resultDependencies = ScalaComponent.builder[(Resolution, Backend)]("Result")
.render_P {
case (res, backend) =>
def infoLabel(label: String) =
<.span(^.`class` := "label label-info", label)
def errorPopOver(label: String, desc: String) =
popOver("danger", label, desc)
def infoPopOver(label: String, desc: String) =
popOver("info", label, desc)
def popOver(`type`: String, label: String, desc: String) =
<.button(^.`type` := "button", ^.`class` := s"btn btn-xs btn-${`type`}",
Attr("data-trigger") := "focus",
Attr("data-toggle") := "popover", Attr("data-placement") := "bottom",
Attr("data-content") := desc,
^.onClick ==> backend.enablePopover,
^.onMouseOver ==> backend.enablePopover,
label
)
def depItem(dep: Dependency, finalVersionOpt: Option[String]) = {
<.tr(
^.`class` := (if (res.errorCache.contains(dep.moduleVersion)) "danger" else ""),
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(finalVersionOpt.fold(dep.version)(finalVersion => s"$finalVersion (for ${dep.version})")),
<.td(TagMod(
if (dep.configuration == "compile") TagMod() else TagMod(infoLabel(dep.configuration)),
if (dep.attributes.`type`.isEmpty || dep.attributes.`type` == "jar") TagMod() else TagMod(infoLabel(dep.attributes.`type`)),
if (dep.attributes.classifier.isEmpty) TagMod() else TagMod(infoLabel(dep.attributes.classifier)),
Some(dep.exclusions).filter(_.nonEmpty).map(excls => infoPopOver("Exclusions", excls.toList.sorted.map{case (org, name) => s"$org:$name"}.mkString("; "))).toSeq.toTagMod,
if (dep.optional) TagMod(infoLabel("optional")) else TagMod(),
res.errorCache.get(dep.moduleVersion).map(errs => errorPopOver("Error", errs.mkString("; "))).toSeq.toTagMod
)),
<.td(TagMod(
res.projectCache.get(dep.moduleVersion) match {
case Some((source: MavenSource, proj)) =>
// FIXME Maven specific, generalize with source.artifacts
val version0 = finalVersionOpt getOrElse dep.version
val relPath =
dep.module.organization.split('.').toSeq ++ Seq(
dep.module.name,
version0,
s"${dep.module.name}-$version0"
)
val root = source.root
TagMod(
<.a(^.href := s"$root${relPath.mkString("/")}.pom",
<.span(^.`class` := "label label-info", "POM")
),
<.a(^.href := s"$root${relPath.mkString("/")}.jar",
<.span(^.`class` := "label label-info", "JAR")
)
)
case _ => TagMod()
}
))
)
}
val sortedDeps = res.minDependencies.toList
.sortBy { dep =>
val (org, name, _) = coursier.core.Module.unapply(dep.module).get
(org, name)
}
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th("Extra"),
<.th("Links")
)
),
<.tbody(
sortedDeps
.map(dep =>
depItem(
dep,
res
.projectCache
.get(dep.moduleVersion)
.map(_._2.version)
.filter(_ != dep.version)
)
)
.toTagMod
)
)
}
.build
object icon {
def apply(id: String) = <.span(^.`class` := s"glyphicon glyphicon-$id", ^.aria.hidden := "true")
def ok = apply("ok")
def edit = apply("pencil")
def remove = apply("remove")
def up = apply("arrow-up")
def down = apply("arrow-down")
}
val moduleEditModal = ScalaComponent.builder[((Module, String), Int, Backend)]("EditModule")
.render_P {
case ((module, version), moduleIdx, backend) =>
<.div(^.`class` := "modal fade", ^.id := "moduleEdit", ^.role := "dialog", ^.aria.labelledBy := "moduleEditTitle",
<.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content",
<.div(^.`class` := "modal-header",
<.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close",
<.span(^.aria.hidden := "true", dangerouslySetInnerHtml := "&times;")
),
<.h4(^.`class` := "modal-title", ^.id := "moduleEditTitle", "Dependency")
),
<.div(^.`class` := "modal-body",
<.form(
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputOrganization", "Organization"),
<.input(^.`class` := "form-control", ^.id := "inputOrganization", ^.placeholder := "Organization",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(organization = value))),
^.value := module.organization
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputName", "Name"),
<.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(name = value))),
^.value := module.name
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputVersion", "Version"),
<.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Version",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(version = value)),
^.value := version
)
),
<.div(^.`class` := "modal-footer",
<.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done")
)
)
)
))
)
}
.build
val modules = ScalaComponent.builder[(Seq[Dependency], Int, Backend)]("Dependencies")
.render_P {
case (deps, editModuleIdx, backend) =>
def depItem(dep: Dependency, idx: Int) =
<.tr(
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(dep.version),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleEdit", ^.`class` := "icon-action",
^.onClick ==> backend.editModule(idx),
icon.edit
)
),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleRemove", ^.`class` := "icon-action",
^.onClick ==> backend.removeModule(idx),
icon.remove
)
)
)
<.div(
<.p(
<.button(^.`type` := "button", ^.`class` := "btn btn-default customButton",
^.onClick ==> backend.addModule,
Attr("data-toggle") := "modal",
Attr("data-target") := "#moduleEdit",
"Add"
)
),
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th(""),
<.th("")
)
),
<.tbody(
deps
.zipWithIndex
.map((depItem _).tupled)
.toTagMod
)
),
moduleEditModal((
deps
.lift(editModuleIdx)
.fold(Module("", "") -> "")(_.moduleVersion),
editModuleIdx,
backend
))
)
}
.build
val repoEditModal = ScalaComponent.builder[((String, MavenRepository), Int, Backend)]("EditRepo")
.render_P {
case ((name, repo), repoIdx, backend) =>
<.div(^.`class` := "modal fade", ^.id := "repoEdit", ^.role := "dialog", ^.aria.labelledBy := "repoEditTitle",
<.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content",
<.div(^.`class` := "modal-header",
<.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close",
<.span(^.aria.hidden := "true", dangerouslySetInnerHtml := "&times;")
),
<.h4(^.`class` := "modal-title", ^.id := "repoEditTitle", "Repository")
),
<.div(^.`class` := "modal-body",
<.form(
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputName", "Name"),
<.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name",
^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (value, item._2)),
^.value := name
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputVersion", "Root"),
<.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Root",
^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (item._1, item._2.copy(root = value))),
^.value := repo.root
)
),
<.div(^.`class` := "modal-footer",
<.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done")
)
)
)
))
)
}
.build
val repositories = ScalaComponent.builder[(Seq[(String, MavenRepository)], Int, Backend)]("Repositories")
.render_P {
case (repos, editRepoIdx, backend) =>
def repoItem(item: (String, MavenRepository), idx: Int, isLast: Boolean) =
<.tr(
<.td(item._1),
<.td(item._2.root),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoEdit", ^.`class` := "icon-action",
^.onClick ==> backend.editRepo(idx),
icon.edit
)
),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoRemove", ^.`class` := "icon-action",
^.onClick ==> backend.removeRepo(idx),
icon.remove
)
),
<.td(
if (idx > 0)
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoUp", ^.`class` := "icon-action",
^.onClick ==> backend.moveRepo(idx, up = true),
icon.up
)
else
TagMod()
),
<.td(
if (isLast)
TagMod()
else
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoDown", ^.`class` := "icon-action",
^.onClick ==> backend.moveRepo(idx, up = false),
icon.down
)
)
)
<.div(
<.p(
<.button(^.`type` := "button", ^.`class` := "btn btn-default customButton",
^.onClick ==> backend.addRepo,
Attr("data-toggle") := "modal",
Attr("data-target") := "#repoEdit",
"Add"
)
),
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Name"),
<.th("Root"),
<.th(""),
<.th(""),
<.th(""),
<.th("")
)
),
<.tbody(
(repos.init.zipWithIndex
.map(t => repoItem(t._1, t._2, isLast = false)) ++
repos.lastOption.map(repoItem(_, repos.length - 1, isLast = true))).toTagMod
)
),
repoEditModal((
repos
.lift(editRepoIdx)
.getOrElse("" -> MavenRepository("")),
editRepoIdx,
backend
))
)
}
.build
val options = ScalaComponent.builder[(ResolutionOptions, Backend)]("ResolutionOptions")
.render_P {
case (options, backend) =>
<.div(
<.div(^.`class` := "checkbox",
<.label(
<.input.checkbox(
^.onChange ==> backend.options.toggleOptional,
if (options.followOptional) ^.checked := true else TagMod()
),
"Follow optional dependencies"
)
)
)
}
.build
val resolution = ScalaComponent.builder[(Option[Resolution], Backend)]("Resolution")
.render_P {
case (resOpt, backend) =>
resOpt match {
case Some(res) =>
<.div(
<.div(^.`class` := "page-header",
<.h1("Resolution")
),
resultDependencies((res, backend))
)
case None =>
<.div()
}
}
.build
val initialState = State(
List(
Dependency(Module("org.apache.spark", "spark-sql_2.11"), "2.2.1") // DEBUG
),
Seq("central" -> MavenRepository("https://repo1.maven.org/maven2/")),
ResolutionOptions(),
None,
-1,
-1,
resolving = false,
reverseTree = false,
log = Nil
)
val app = ScalaComponent.builder[Unit]("Coursier")
.initialState(initialState)
.backend(new Backend(_))
.render { scope =>
val S = scope.state
val backend = scope.backend
<.div(
<.div(^.role := "tabpanel",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist",
<.li(^.role := "presentation", ^.`class` := "active",
<.a(^.href := "#dependencies", ^.aria.controls := "dependencies", ^.role := "tab", Attr("data-toggle") := "tab",
s"Dependencies (${S.modules.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#repositories", ^.aria.controls := "repositories", ^.role := "tab", Attr("data-toggle") := "tab",
s"Repositories (${S.repositories.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#options", ^.aria.controls := "options", ^.role := "tab", Attr("data-toggle") := "tab",
"Options"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane active", ^.id := "dependencies",
modules((S.modules, S.editModuleIdx, backend))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "repositories",
repositories((S.repositories, S.editRepoIdx, backend))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "options",
options((S.options, backend))
)
)
),
<.div(<.form(^.onSubmit ==> backend.handleResolve,
<.button(^.`type` := "submit", ^.id := "resolveButton", ^.`class` := "btn btn-lg btn-primary",
^.disabled := S.resolving,
if (S.resolving) "Resolving..." else "Resolve"
)
)),
<.div(^.role := "tabpanel", ^.id := "results",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist", ^.id := "resTabs",
<.li(^.role := "presentation", ^.id := "resResTab",
<.a(^.href := "#resolution", ^.aria.controls := "resolution", ^.role := "tab", Attr("data-toggle") := "tab",
"Resolution"
)
),
<.li(^.role := "presentation", ^.id := "resLogTab",
<.a(^.href := "#log", ^.aria.controls := "log", ^.role := "tab", Attr("data-toggle") := "tab",
"Log"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#depgraph", ^.aria.controls := "depgraph", ^.role := "tab", Attr("data-toggle") := "tab",
"Graph"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#deptreepanel", ^.aria.controls := "deptreepanel", ^.role := "tab", Attr("data-toggle") := "tab",
"Tree"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "resolution",
resolution((S.resolutionOpt, backend))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "log",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> backend.clearLog,
"Clear"
),
<.div(^.`class` := "well",
<.ul(^.`class` := "log",
S.log.map(e => <.li(e)).toTagMod
)
)
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "depgraph",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> backend.updateDepGraphBtn(S.resolutionOpt.getOrElse(Resolution.empty)),
"Redraw"
),
<.div(^.id := "depgraphcanvas")
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "deptreepanel",
<.div(^.`class` := "checkbox",
<.label(
<.input.checkbox(
^.onChange ==> backend.toggleReverseTree,
if (S.reverseTree) ^.checked := true else TagMod()
),
"Reverse"
)
),
<.div(^.id := "deptree")
)
)
)
)
}
.build
}

View File

@ -2,42 +2,30 @@ package coursier.web
import coursier.{Dependency, Fetch, MavenRepository, Module, Platform, Repository, Resolution}
import coursier.maven.MavenSource
import coursier.util.{Gather, Task}
import japgolly.scalajs.react.vdom.{ TagMod, Attr }
import japgolly.scalajs.react.vdom.Attrs.dangerouslySetInnerHtml
import japgolly.scalajs.react.{ ReactEventI, ReactComponentB, BackendScope }
import japgolly.scalajs.react.vdom.prefix_<^._
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
import coursier.util.{EitherT, Gather, Task}
import japgolly.scalajs.react._
import org.scalajs.dom
import org.scalajs.jquery.jQuery
import scala.concurrent.Future
import scala.scalajs.js
import js.Dynamic.{ global => g }
import scala.util.{Failure, Success}
import js.Dynamic.{global => g}
final case class ResolutionOptions(
followOptional: Boolean = false
)
final case class State(
modules: Seq[Dependency],
repositories: Seq[(String, MavenRepository)],
options: ResolutionOptions,
resolutionOpt: Option[Resolution],
editModuleIdx: Int,
editRepoIdx: Int,
resolving: Boolean,
reverseTree: Boolean,
log: Seq[String]
)
class Backend($: BackendScope[Unit, State]) {
final class Backend($: BackendScope[_, State]) {
def fetch(
repositories: Seq[Repository],
fetch: Fetch.Content[Task]
): Fetch.Metadata[Task] = {
val fetch0: Fetch.Content[Task] = { a =>
if (a.url.endsWith("/"))
// don't fetch directory listings
EitherT[Task, String, String](Task.point(Left("")))
else
fetch(a)
}
modVers => Gather[Task].gather(
modVers.map { case (module, version) =>
Fetch.find(repositories, module, version, fetch)
@ -102,7 +90,7 @@ class Backend($: BackendScope[Unit, State]) {
println("Rendered canvas")
}
def updateDepGraphBtn(resolution: Resolution)(e: ReactEventI) = {
def updateDepGraphBtn(resolution: Resolution)(e: raw.SyntheticEvent[_]) = CallbackTo[Unit] {
updateDepGraph(resolution)
}
@ -150,81 +138,95 @@ class Backend($: BackendScope[Unit, State]) {
.treeview(js.Dictionary("data" -> js.Array(minDependencies.toList.map(tree): _*)))
}
def resolve(action: => Unit = ()) = {
def resolve(action: => Unit = ()): CallbackTo[Unit] = {
g.$("#resLogTab a:last").tab("show")
$.modState(_.copy(resolving = true, log = Nil))
$.modState(_.copy(resolving = true, log = Nil)).runNow()
val logger: Platform.Logger = new Platform.Logger {
def fetched(url: String) = {
println(s"<- $url")
$.modState(s => s.copy(log = s"<- $url" +: s.log))
$.modState(s => s.copy(log = s"<- $url" +: s.log)).runNow()
}
def fetching(url: String) = {
println(s"-> $url")
$.modState(s => s.copy(log = s"-> $url" +: s.log))
$.modState(s => s.copy(log = s"-> $url" +: s.log)).runNow()
}
def other(url: String, msg: String) = {
println(s"$url: $msg")
$.modState(s => s.copy(log = s"$url: $msg" +: s.log))
$.modState(s => s.copy(log = s"$url: $msg" +: s.log)).runNow()
}
}
val s = $.state
def task = {
val res = coursier.Resolution(
s.modules.toSet,
filter = Some(dep =>
s.options.followOptional || !dep.optional
$.state.map { s =>
def task = {
val res = coursier.Resolution(
s.modules.toSet,
filter = Some(dep =>
s.options.followOptional || !dep.optional
)
)
)
res
.process
.run(fetch(s.repositories.map { case (_, repo) => repo }, Platform.artifactWithLogger(logger)), 100)
}
// For reasons that are unclear to me, not delaying this when using the runNow execution context
// somehow discards the $.modState above. (Not a major problem as queue is used by default.)
Future(task)(scala.scalajs.concurrent.JSExecutionContext.Implicits.queue).flatMap(_.future()).foreach { res: Resolution =>
$.modState{ s =>
updateDepGraph(res)
updateTree(res, "#deptree", reverse = s.reverseTree)
s.copy(
resolutionOpt = Some(res),
resolving = false
)
res
.process
.run(fetch(s.repositories.map { case (_, repo) => repo }, Platform.artifactWithLogger(logger)), 100)
}
g.$("#resResTab a:last")
.tab("show")
implicit val ec = scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
task.map { res: Resolution =>
$.modState { s =>
updateDepGraph(res)
updateTree(res, "#deptree", reverse = s.reverseTree)
s.copy(
resolutionOpt = Some(res),
resolving = false
)
}.runNow()
g.$("#resResTab a:last")
.tab("show")
}.future()(ec).onComplete {
case Success(_) =>
case Failure(t) =>
println(s"Caught exception: $t")
}
()
}
}
def handleResolve(e: ReactEventI) = {
println(s"Resolving")
e.preventDefault()
jQuery("#results").css("display", "block")
resolve()
def handleResolve(e: raw.SyntheticEvent[_]) = {
val c = CallbackTo[Unit] {
println(s"Resolving")
e.preventDefault()
jQuery("#results").css("display", "block")
}
c.flatMap { _ =>
resolve()
}
}
def clearLog(e: ReactEventI) = {
def clearLog(e: raw.SyntheticEvent[_]) = {
$.modState(_.copy(log = Nil))
}
def toggleReverseTree(e: ReactEventI) = {
$.modState{ s =>
def toggleReverseTree(e: raw.SyntheticEvent[_]) =
$.modState { s =>
for (res <- s.resolutionOpt)
updateTree(res, "#deptree", reverse = !s.reverseTree)
s.copy(reverseTree = !s.reverseTree)
}
}
def editModule(idx: Int)(e: ReactEventI) = {
def editModule(idx: Int)(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState(_.copy(editModuleIdx = idx))
}
def removeModule(idx: Int)(e: ReactEventI) = {
def removeModule(idx: Int)(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState(s =>
s.copy(
@ -236,21 +238,22 @@ class Backend($: BackendScope[Unit, State]) {
)
}
def updateModule(moduleIdx: Int, update: (Dependency, String) => Dependency)(e: ReactEventI) = {
def updateModule(moduleIdx: Int, update: (Dependency, String) => Dependency)(e: raw.SyntheticEvent[dom.raw.HTMLInputElement]) =
if (moduleIdx >= 0) {
$.modState{ state =>
e.persist()
$.modState { state =>
val dep = state.modules(moduleIdx)
state.copy(
modules = state.modules
.updated(moduleIdx, update(dep, e.target.value))
)
}
}
}
} else
CallbackTo.pure(())
def addModule(e: ReactEventI) = {
def addModule(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState{ state =>
$.modState { state =>
val modules = state.modules :+ Dependency(Module("", ""), "")
println(s"Modules:\n${modules.mkString("\n")}")
state.copy(
@ -260,12 +263,12 @@ class Backend($: BackendScope[Unit, State]) {
}
}
def editRepo(idx: Int)(e: ReactEventI) = {
def editRepo(idx: Int)(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState(_.copy(editRepoIdx = idx))
}
def removeRepo(idx: Int)(e: ReactEventI) = {
def removeRepo(idx: Int)(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState(s =>
s.copy(
@ -277,7 +280,7 @@ class Backend($: BackendScope[Unit, State]) {
)
}
def moveRepo(idx: Int, up: Boolean)(e: ReactEventI) = {
def moveRepo(idx: Int, up: Boolean)(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState { s =>
val idx0 = if (up) idx - 1 else idx + 1
@ -297,21 +300,21 @@ class Backend($: BackendScope[Unit, State]) {
}
}
def updateRepo(repoIdx: Int, update: ((String, MavenRepository), String) => (String, MavenRepository))(e: ReactEventI) = {
if (repoIdx >= 0) {
$.modState{ state =>
def updateRepo(repoIdx: Int, update: ((String, MavenRepository), String) => (String, MavenRepository))(e: raw.SyntheticEvent[dom.raw.HTMLInputElement]) =
if (repoIdx >= 0)
$.modState { state =>
val repo = state.repositories(repoIdx)
state.copy(
repositories = state.repositories
.updated(repoIdx, update(repo, e.target.value))
)
}
}
}
else
CallbackTo.pure(())
def addRepo(e: ReactEventI) = {
def addRepo(e: raw.SyntheticEvent[_]) = {
e.preventDefault()
$.modState{ state =>
$.modState { state =>
val repositories = state.repositories :+ ("" -> MavenRepository(""))
println(s"Repositories:\n${repositories.mkString("\n")}")
state.copy(
@ -321,13 +324,13 @@ class Backend($: BackendScope[Unit, State]) {
}
}
def enablePopover(e: ReactEventI) = {
def enablePopover(e: raw.SyntheticMouseEvent[_]) = CallbackTo[Unit] {
g.$("[data-toggle='popover']")
.popover()
}
object options {
def toggleOptional(e: ReactEventI) = {
def toggleOptional(e: raw.SyntheticEvent[_]) = {
$.modState(s =>
s.copy(
options = s.options
@ -337,485 +340,3 @@ class Backend($: BackendScope[Unit, State]) {
}
}
}
object App {
lazy val arbor = g.arbor
val resultDependencies = ReactComponentB[(Resolution, Backend)]("Result")
.render{ T =>
val (res, backend) = T
def infoLabel(label: String) =
<.span(^.`class` := "label label-info", label)
def errorPopOver(label: String, desc: String) =
popOver("danger", label, desc)
def infoPopOver(label: String, desc: String) =
popOver("info", label, desc)
def popOver(`type`: String, label: String, desc: String) =
<.button(^.`type` := "button", ^.`class` := s"btn btn-xs btn-${`type`}",
Attr("data-trigger") := "focus",
Attr("data-toggle") := "popover", Attr("data-placement") := "bottom",
Attr("data-content") := desc,
^.onClick ==> backend.enablePopover,
^.onMouseOver ==> backend.enablePopover,
label
)
def depItem(dep: Dependency, finalVersionOpt: Option[String]) = {
<.tr(
^.`class` := (if (res.errorCache.contains(dep.moduleVersion)) "danger" else ""),
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(finalVersionOpt.fold(dep.version)(finalVersion => s"$finalVersion (for ${dep.version})")),
<.td(Seq[Seq[TagMod]](
if (dep.configuration == "compile") Seq() else Seq(infoLabel(dep.configuration)),
if (dep.attributes.`type`.isEmpty || dep.attributes.`type` == "jar") Seq() else Seq(infoLabel(dep.attributes.`type`)),
if (dep.attributes.classifier.isEmpty) Seq() else Seq(infoLabel(dep.attributes.classifier)),
Some(dep.exclusions).filter(_.nonEmpty).map(excls => infoPopOver("Exclusions", excls.toList.sorted.map{case (org, name) => s"$org:$name"}.mkString("; "))).toSeq,
if (dep.optional) Seq(infoLabel("optional")) else Seq(),
res.errorCache.get(dep.moduleVersion).map(errs => errorPopOver("Error", errs.mkString("; "))).toSeq
)),
<.td(Seq[Seq[TagMod]](
res.projectCache.get(dep.moduleVersion) match {
case Some((source: MavenSource, proj)) =>
// FIXME Maven specific, generalize with source.artifacts
val version0 = finalVersionOpt getOrElse dep.version
val relPath =
dep.module.organization.split('.').toSeq ++ Seq(
dep.module.name,
version0,
s"${dep.module.name}-$version0"
)
val root = source.root
Seq(
<.a(^.href := s"$root${relPath.mkString("/")}.pom",
<.span(^.`class` := "label label-info", "POM")
),
<.a(^.href := s"$root${relPath.mkString("/")}.jar",
<.span(^.`class` := "label label-info", "JAR")
)
)
case _ => Seq()
}
))
)
}
val sortedDeps = res.minDependencies.toList
.sortBy { dep =>
val (org, name, _) = coursier.core.Module.unapply(dep.module).get
(org, name)
}
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th("Extra"),
<.th("Links")
)
),
<.tbody(
sortedDeps.map(dep =>
depItem(
dep,
res
.projectCache
.get(dep.moduleVersion)
.map(_._2.version)
.filter(_ != dep.version)
)
)
)
)
}
.build
object icon {
def apply(id: String) = <.span(^.`class` := s"glyphicon glyphicon-$id", ^.aria.hidden := "true")
def ok = apply("ok")
def edit = apply("pencil")
def remove = apply("remove")
def up = apply("arrow-up")
def down = apply("arrow-down")
}
val moduleEditModal = ReactComponentB[((Module, String), Int, Backend)]("EditModule")
.render{ P =>
val ((module, version), moduleIdx, backend) = P
<.div(^.`class` := "modal fade", ^.id := "moduleEdit", ^.role := "dialog", ^.aria.labelledby := "moduleEditTitle",
<.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content",
<.div(^.`class` := "modal-header",
<.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close",
<.span(^.aria.hidden := "true", dangerouslySetInnerHtml("&times;"))
),
<.h4(^.`class` := "modal-title", ^.id := "moduleEditTitle", "Dependency")
),
<.div(^.`class` := "modal-body",
<.form(
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputOrganization", "Organization"),
<.input(^.`class` := "form-control", ^.id := "inputOrganization", ^.placeholder := "Organization",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(organization = value))),
^.value := module.organization
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputName", "Name"),
<.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(name = value))),
^.value := module.name
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputVersion", "Version"),
<.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Version",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(version = value)),
^.value := version
)
),
<.div(^.`class` := "modal-footer",
<.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done")
)
)
)
))
)
}
.build
val modules = ReactComponentB[(Seq[Dependency], Int, Backend)]("Dependencies")
.render{ P =>
val (deps, editModuleIdx, backend) = P
def depItem(dep: Dependency, idx: Int) =
<.tr(
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(dep.version),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleEdit", ^.`class` := "icon-action",
^.onClick ==> backend.editModule(idx),
icon.edit
)
),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleRemove", ^.`class` := "icon-action",
^.onClick ==> backend.removeModule(idx),
icon.remove
)
)
)
<.div(
<.p(
<.button(^.`type` := "button", ^.`class` := "btn btn-default customButton",
^.onClick ==> backend.addModule,
Attr("data-toggle") := "modal",
Attr("data-target") := "#moduleEdit",
"Add"
)
),
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th(""),
<.th("")
)
),
<.tbody(
deps.zipWithIndex
.map((depItem _).tupled)
)
),
moduleEditModal((
deps
.lift(editModuleIdx)
.fold(Module("", "") -> "")(_.moduleVersion),
editModuleIdx,
backend
))
)
}
.build
val repoEditModal = ReactComponentB[((String, MavenRepository), Int, Backend)]("EditRepo")
.render{ P =>
val ((name, repo), repoIdx, backend) = P
<.div(^.`class` := "modal fade", ^.id := "repoEdit", ^.role := "dialog", ^.aria.labelledby := "repoEditTitle",
<.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content",
<.div(^.`class` := "modal-header",
<.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close",
<.span(^.aria.hidden := "true", dangerouslySetInnerHtml("&times;"))
),
<.h4(^.`class` := "modal-title", ^.id := "repoEditTitle", "Repository")
),
<.div(^.`class` := "modal-body",
<.form(
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputName", "Name"),
<.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name",
^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (value, item._2)),
^.value := name
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputVersion", "Root"),
<.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Root",
^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (item._1, item._2.copy(root = value))),
^.value := repo.root
)
),
<.div(^.`class` := "modal-footer",
<.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done")
)
)
)
))
)
}
.build
val repositories = ReactComponentB[(Seq[(String, MavenRepository)], Int, Backend)]("Repositories")
.render{ P =>
val (repos, editRepoIdx, backend) = P
def repoItem(item: (String, MavenRepository), idx: Int, isLast: Boolean) =
<.tr(
<.td(item._1),
<.td(item._2.root),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoEdit", ^.`class` := "icon-action",
^.onClick ==> backend.editRepo(idx),
icon.edit
)
),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoRemove", ^.`class` := "icon-action",
^.onClick ==> backend.removeRepo(idx),
icon.remove
)
),
<.td(
if (idx > 0)
Seq(<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoUp", ^.`class` := "icon-action",
^.onClick ==> backend.moveRepo(idx, up = true),
icon.up
))
else
Seq()
),
<.td(
if (isLast)
Seq()
else
Seq(<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoDown", ^.`class` := "icon-action",
^.onClick ==> backend.moveRepo(idx, up = false),
icon.down
))
)
)
<.div(
<.p(
<.button(^.`type` := "button", ^.`class` := "btn btn-default customButton",
^.onClick ==> backend.addRepo,
Attr("data-toggle") := "modal",
Attr("data-target") := "#repoEdit",
"Add"
)
),
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Name"),
<.th("Root"),
<.th(""),
<.th(""),
<.th(""),
<.th("")
)
),
<.tbody(
repos.init.zipWithIndex
.map(t => repoItem(t._1, t._2, isLast = false)) ++
repos.lastOption.map(repoItem(_, repos.length - 1, isLast = true))
)
),
repoEditModal((
repos
.lift(editRepoIdx)
.getOrElse("" -> MavenRepository("")),
editRepoIdx,
backend
))
)
}
.build
val options = ReactComponentB[(ResolutionOptions, Backend)]("ResolutionOptions")
.render{ P =>
val (options, backend) = P
<.div(
<.div(^.`class` := "checkbox",
<.label(
<.input(^.`type` := "checkbox",
^.onChange ==> backend.options.toggleOptional,
if (options.followOptional) Seq(^.checked := "checked") else Seq(),
"Follow optional dependencies"
)
)
)
)
}
.build
val resolution = ReactComponentB[(Option[Resolution], Backend)]("Resolution")
.render{ T =>
val (resOpt, backend) = T
resOpt match {
case Some(res) =>
<.div(
<.div(^.`class` := "page-header",
<.h1("Resolution")
),
resultDependencies((res, backend))
)
case None =>
<.div()
}
}
.build
val initialState = State(
Nil,
Seq("central" -> MavenRepository("https://repo1.maven.org/maven2/")),
ResolutionOptions(),
None,
-1,
-1,
resolving = false,
reverseTree = false,
log = Nil
)
val app = ReactComponentB[Unit]("Coursier")
.initialState(initialState)
.backend(new Backend(_))
.render((_,S,B) =>
<.div(
<.div(^.role := "tabpanel",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist",
<.li(^.role := "presentation", ^.`class` := "active",
<.a(^.href := "#dependencies", ^.aria.controls := "dependencies", ^.role := "tab", Attr("data-toggle") := "tab",
s"Dependencies (${S.modules.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#repositories", ^.aria.controls := "repositories", ^.role := "tab", Attr("data-toggle") := "tab",
s"Repositories (${S.repositories.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#options", ^.aria.controls := "options", ^.role := "tab", Attr("data-toggle") := "tab",
"Options"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane active", ^.id := "dependencies",
modules((S.modules, S.editModuleIdx, B))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "repositories",
repositories((S.repositories, S.editRepoIdx, B))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "options",
options((S.options, B))
)
)
),
<.div(<.form(^.onSubmit ==> B.handleResolve,
<.button(^.`type` := "submit", ^.id := "resolveButton", ^.`class` := "btn btn-lg btn-primary",
if (S.resolving) ^.disabled := "true" else Attr("active") := "true",
if (S.resolving) "Resolving..." else "Resolve"
)
)),
<.div(^.role := "tabpanel", ^.id := "results",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist", ^.id := "resTabs",
<.li(^.role := "presentation", ^.id := "resResTab",
<.a(^.href := "#resolution", ^.aria.controls := "resolution", ^.role := "tab", Attr("data-toggle") := "tab",
"Resolution"
)
),
<.li(^.role := "presentation", ^.id := "resLogTab",
<.a(^.href := "#log", ^.aria.controls := "log", ^.role := "tab", Attr("data-toggle") := "tab",
"Log"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#depgraph", ^.aria.controls := "depgraph", ^.role := "tab", Attr("data-toggle") := "tab",
"Graph"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#deptreepanel", ^.aria.controls := "deptreepanel", ^.role := "tab", Attr("data-toggle") := "tab",
"Tree"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "resolution",
resolution((S.resolutionOpt, B))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "log",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> B.clearLog,
"Clear"
),
<.div(^.`class` := "well",
<.ul(^.`class` := "log",
S.log.map(e => <.li(e))
)
)
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "depgraph",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> B.updateDepGraphBtn(S.resolutionOpt.getOrElse(Resolution.empty)),
"Redraw"
),
<.div(^.id := "depgraphcanvas")
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "deptreepanel",
<.div(^.`class` := "checkbox",
<.label(
<.input(^.`type` := "checkbox",
^.onChange ==> B.toggleReverseTree,
if (S.reverseTree) Seq(^.checked := "checked") else Seq(),
"Reverse"
)
)
),
<.div(^.id := "deptree")
)
)
)
)
)
.buildU
}

View File

@ -1,14 +1,12 @@
package coursier.web
import japgolly.scalajs.react.React
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
import org.scalajs.dom.document
@JSExportTopLevel("CoursierWeb")
object Main {
@JSExport
def main(): Unit = {
React.render(App.app("Coursier"), document.getElementById("demoContent"))
}
def main(): Unit =
App.app()
.renderIntoDOM(document.getElementById("demoContent"))
}

View File

@ -0,0 +1,5 @@
package coursier.web
final case class ResolutionOptions(
followOptional: Boolean = false
)

View File

@ -0,0 +1,15 @@
package coursier.web
import coursier.{Dependency, MavenRepository, Resolution}
final case class State(
modules: Seq[Dependency],
repositories: Seq[(String, MavenRepository)],
options: ResolutionOptions,
resolutionOpt: Option[Resolution],
editModuleIdx: Int,
editRepoIdx: Int,
resolving: Boolean,
reverseTree: Boolean,
log: Seq[String]
)