mirror of https://github.com/sbt/sbt.git
Switch to latest scalajs-react, split web module sources (#906)
This commit is contained in:
parent
dd7c8c19d4
commit
528c2adc2f
|
|
@ -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".
|
||||
|
|
|
|||
17
build.sbt
17
build.sbt
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 := "×")
|
||||
),
|
||||
<.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 := "×")
|
||||
),
|
||||
<.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
|
||||
|
||||
}
|
||||
|
|
@ -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("×"))
|
||||
),
|
||||
<.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("×"))
|
||||
),
|
||||
<.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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package coursier.web
|
||||
|
||||
final case class ResolutionOptions(
|
||||
followOptional: Boolean = false
|
||||
)
|
||||
|
|
@ -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]
|
||||
)
|
||||
Loading…
Reference in New Issue