diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5501b3 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +Chrome System Monitor +========= +Simple example of chrome application. + +## For developers +To create unpacked application: + +``` +sbt +chromeUnpackedFast +```` +You can find result in `./target/chrome/unpacked-fast` folder. + +After you install unpacked extension to Chrome, it can be found on `chrome://apps` page. \ No newline at end of file diff --git a/build.sbt b/build.sbt index 414a651..c8ac376 100644 --- a/build.sbt +++ b/build.sbt @@ -1,56 +1,54 @@ -import chrome.Impl._ -import chrome.permissions.APIPermission._ +import Dependencies.{addLibraries, addJsLibraries} +import chrome.permissions.Permission +import chrome.permissions.Permission.API +import chrome.{App, AppManifest, Background} import net.lullabyte.{Chrome, ChromeSbtPlugin} -lazy val root = project.in(file(".")) - .enablePlugins(ChromeSbtPlugin) - .settings( - name := "System Monitor", - version := "0.1.0", - scalaVersion := "2.11.8", - scalacOptions ++= Seq( - "-language:implicitConversions", - "-language:existentials", - "-Xlint", - "-deprecation", - "-Xfatal-warnings", - "-feature" - ), - persistLauncher := true, - persistLauncher in Test := false, - relativeSourceMaps := true, - libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % "0.9.0" withSources() withJavadoc(), - "com.github.japgolly.scalajs-react" %%% "core" % "0.9.1" withSources() withJavadoc(), - "com.github.japgolly.scalajs-react" %%% "extra" % "0.9.1" withSources() withJavadoc(), - "com.github.japgolly.scalacss" %%% "core" % "0.3.0" withSources() withJavadoc(), - "com.github.japgolly.scalacss" %%% "ext-react" % "0.3.0" withSources() withJavadoc(), - "net.lullabyte" %%% "scala-js-chrome" % "0.2.0" withSources() withJavadoc() - ), - jsDependencies += "org.webjars" % "react" % "0.13.3" / "react-with-addons.min.js" commonJSName "React", - skip in packageJSDependencies := false, - chromeManifest := AppManifest( - name = name.value, - version = version.value, - app = App( - background = Background( - scripts = List("deps.js", "main.js", "launcher.js") - ) - ), - defaultLocale = Some("en"), - icons = Chrome.icons( - "assets/icons", - "app.png", - Set(16, 32, 48, 64, 96, 128, 256, 512) - ), - permissions = Set( - System.CPU, - System.Display, - System.Memory, - System.Network, - Storage - ) +enablePlugins(ChromeSbtPlugin) + +name := "System Monitor" +version := "0.2.1" +scalaVersion := "2.12.4" + + +addLibraries() +addJsLibraries() + + +scalaJSUseMainModuleInitializer := true +scalaJSUseMainModuleInitializer in Test := false +relativeSourceMaps := true +skip in packageJSDependencies := false + + +scalacOptions ++= MyBuildConfig.scalacOptions + + +chromeManifest := new AppManifest { + val name = Keys.name.value + val version = Keys.version.value + val app = App( + background = Background( + scripts = List("dependencies.js", "main.js") ) ) + override val defaultLocale = Some("en") + + override val icons = Chrome.icons( + "assets/icons", + "app.png", + Set(16, 32, 48, 64, 96, 128, 256, 512) + ) + + override val permissions: Set[Permission] = Set( + API.System.CPU, + API.System.Display, + API.System.Memory, + API.System.Network, + API.Storage + ) +} + + diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..fe5e063 --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,52 @@ +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import sbt.Keys.libraryDependencies +import sbt._ + +object Dependencies { + + object versions { + val scalacss = "0.5.6" + val scalaReact = "1.4.2" + + val react = "16.7.0" + } + + def addLibraries() = { + libraryDependencies ++= Seq( + "org.scala-js" %%% "scalajs-dom" % "0.9.2" withSources() withJavadoc(), + "com.github.japgolly.scalajs-react" %%% "core" % versions.scalaReact withSources() withJavadoc(), + "com.github.japgolly.scalajs-react" %%% "extra" % versions.scalaReact withSources() withJavadoc(), + "com.github.japgolly.scalacss" %%% "core" % versions.scalacss withSources() withJavadoc(), + "com.github.japgolly.scalacss" %%% "ext-react" % versions.scalacss withSources() withJavadoc(), + "net.lullabyte" %%% "scala-js-chrome" % "0.5.0" withSources() withJavadoc() + ) + } + + def addJsLibraries() = { + jsDependencies ++= Seq( + "org.webjars.npm" % "react" % versions.react + / "umd/react.development.js" + minified "umd/react.production.min.js" + commonJSName "React", + + "org.webjars.npm" % "react-dom" % versions.react + / "umd/react-dom.development.js" + minified "umd/react-dom.production.min.js" + dependsOn "umd/react.development.js" + commonJSName "ReactDOM" + ) + } +} + +object MyBuildConfig { + + val scalacOptions = Seq( + "-language:implicitConversions", + "-language:existentials", + "-Xlint", + "-deprecation", + "-Xfatal-warnings", + "-feature" + ) + +} \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index 19623ba..c288de4 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.8 +sbt.version = 1.3.4 diff --git a/project/plugins.sbt b/project/plugins.sbt index 7c13a71..b27191e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,4 @@ -addSbtPlugin("net.lullabyte" % "sbt-chrome-plugin" % "0.2.1") -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0") +resolvers += Resolver.bintrayIvyRepo("veinhorn", "sbt-plugins") + +addSbtPlugin("net.lullabyte" % "sbt-chrome-plugin" % "0.5.8") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") diff --git a/scalastyle-config.xml b/scalastyle-config.xml index 7e3596f..3432d70 100644 --- a/scalastyle-config.xml +++ b/scalastyle-config.xml @@ -6,7 +6,7 @@ - + ReactTag]) + } def centered(element: TagMod) = div( display := "flex", @@ -34,11 +36,9 @@ object App { def webgl = { val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] val gl = canvas.getContext("webgl").asInstanceOf[dom.webgl.RenderingContext] - div( - for (ext <- gl.getSupportedExtensions()) yield { - div(ext) - } - ) + val extensionTags = gl.getSupportedExtensions().map(ext => div(ext)) + + div(extensionTags:_*) } val modules = List( @@ -49,27 +49,25 @@ object App { About ) - val component = ReactComponentB[Unit]("App") + val component = ScalaComponent.builder[Unit]("App") .initialState(State(Some(CPU))) - .backend(new Backend(_)) - .render((p, s, b) => { - div(style.app)( - div(style.sidebar)( - for (module <- modules) yield { - div( - onClick --> { - b.select(module) - }, - style.menuItem(s.currentView.map(_ == module).getOrElse(false)) - )( - img(style.menuItemIcon)(src := module.iconUrl) - ) - } - ), - div(style.viewStyle)( - s.currentView.map(_.component).getOrElse(empty) + .backend(Backend.apply) + .render(lifecycle => { + div(style.app)( + div(style.sidebar)(moduleTags(lifecycle): _*), + div(style.viewStyle)( + lifecycle.state.currentView.map(_.component).getOrElse(empty) + ) ) - ) - }).buildU + }).build + + def moduleTags(lifecycle: Lifecycle.RenderScope[Unit, State, Backend]): Seq[TagMod] = modules.map { module => + div( + onClick --> lifecycle.backend.select(module), + style.menuItem(lifecycle.state.currentView.contains(module)) + )( + img(style.menuItemIcon)(src := module.iconUrl) + ) + } } diff --git a/src/main/scala/monitor/SystemMonitor.scala b/src/main/scala/monitor/SystemMonitor.scala index daaaa9f..4657dcb 100644 --- a/src/main/scala/monitor/SystemMonitor.scala +++ b/src/main/scala/monitor/SystemMonitor.scala @@ -1,25 +1,28 @@ package monitor -import styles.Default import chrome.app.runtime.bindings.LaunchData -import chrome.app.window.bindings.{BoundsSpecification, CreateWindowOptions} import chrome.app.window._ -import japgolly.scalajs.react.React -import org.scalajs.dom.raw.HTMLStyleElement +import chrome.app.window.bindings.CreateWindowOptions +import chrome.utils.ChromeApp +import monitor.styles.Default import org.scalajs.dom +import org.scalajs.dom.raw.HTMLStyleElement + import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue -import scalacss.Defaults._ +import scalacss.DevDefaults._ import scalacss.ScalaCssReact._ -object SystemMonitor extends utils.ChromeApp { +object SystemMonitor extends ChromeApp { override def onLaunched(launchData: LaunchData): Unit = { val options = CreateWindowOptions(id = "MainWindow") + Window.create("assets/html/App.html", options).foreach { window => - window.contentWindow.onload = (e: dom.Event) => { + window.contentWindow.onload = (_: dom.Event) => { val style = Default.render[HTMLStyleElement] window.contentWindow.document.head.appendChild(style) - React.render(App.component(), window.contentWindow.document.body) + + App.component().renderIntoDOM(window.contentWindow.document.body) } } } diff --git a/src/main/scala/monitor/Timeline.scala b/src/main/scala/monitor/Timeline.scala index af3a722..f4b350f 100644 --- a/src/main/scala/monitor/Timeline.scala +++ b/src/main/scala/monitor/Timeline.scala @@ -5,6 +5,10 @@ import scala.concurrent.duration.FiniteDuration import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue import scala.scalajs.js import chrome.events.EventSource +import japgolly.scalajs.react.{Callback, CallbackTo} + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer class TickSource[T](val sampleInterval: FiniteDuration, fun: => Future[T]) extends EventSource[T] { @@ -22,7 +26,7 @@ class TickSource[T](val sampleInterval: FiniteDuration, fun: => Future[T]) exten object Timeline { trait Listener[T] { - def update(value: T): Unit + def update(value: T): CallbackTo[Unit] } } @@ -31,32 +35,36 @@ class Timeline[T](val sampleCount: Int, val sampleInterval: FiniteDuration)(fun: import Timeline._ - private var _samples: List[T] = List() - private var listeners = collection.mutable.ListBuffer[Listener[Timeline[T]]]() + private var _samples: List[T] = List.empty + private val listeners = mutable.ListBuffer[Listener[Timeline[T]]]() private var intervalHandler: Option[js.timers.SetIntervalHandle] = None - private def addSample(sample: T): Unit = { + private def addSample(sample: T): ListBuffer[CallbackTo[Unit]] = { _samples = (sample :: _samples).take(sampleCount) listeners.map(_.update(this)) } def samples = _samples - def addListener(listener: Listener[Timeline[T]]) = { + def addListener(listener: Listener[Timeline[T]]) = Callback { listeners += listener } - def removeListener(listener: Listener[Timeline[T]]) = { + def removeListener(listener: Listener[Timeline[T]]) = Callback { listeners -= listener } - private def tick(): Unit = { - fun.onSuccess { case s => addSample(s) } - } + private def tick(): Future[Unit] = for { + sample <- fun + _ <- Future.traverse(addSample(sample))(_.asAsyncCallback.unsafeToFuture()) + } yield () + + + def start() = { + if (intervalHandler.isEmpty) + intervalHandler = Some(js.timers.setInterval(sampleInterval)(tick())) - def start() = intervalHandler match { - case None => intervalHandler = Some(js.timers.setInterval(sampleInterval)(tick)) - case _ => + this } def stop() = intervalHandler foreach js.timers.clearInterval diff --git a/src/main/scala/monitor/modules/About.scala b/src/main/scala/monitor/modules/About.scala index 31648a9..605fabb 100644 --- a/src/main/scala/monitor/modules/About.scala +++ b/src/main/scala/monitor/modules/About.scala @@ -1,6 +1,7 @@ package monitor.modules import japgolly.scalajs.react.vdom.all._ +import monitor.ui object About extends Module { diff --git a/src/main/scala/monitor/modules/CPU.scala b/src/main/scala/monitor/modules/CPU.scala index 3c1b9a2..2ead89c 100644 --- a/src/main/scala/monitor/modules/CPU.scala +++ b/src/main/scala/monitor/modules/CPU.scala @@ -2,10 +2,10 @@ package monitor.modules import chrome.system.cpu.bindings.{CPUInfo, Processor} import japgolly.scalajs.react.vdom.all._ -import japgolly.scalajs.react.{BackendScope, ReactComponentB} -import monitor.Timeline +import japgolly.scalajs.react.{BackendScope, CallbackTo, ScalaComponent} import monitor.Timeline.Listener import monitor.math._ +import monitor.{Timeline, ui} import monitor.ui.components.SVGDiagram import scala.concurrent.duration._ @@ -16,22 +16,22 @@ object CPU extends Module { val iconUrl = "/assets/icons/scalable/cpu.svg" val cpuTimeline = { - val timeline = new Timeline[chrome.system.cpu.bindings.CPUInfo](61, 1.second)({ + val timeline = new Timeline[CPUInfo](61, 1.second)( chrome.system.cpu.CPU.getInfo - }) + ) timeline.start() timeline } - def deltas(history: List[CPUInfo]): List[List[UsageDelta]] = { + private def deltas(history: List[CPUInfo]): List[List[UsageDelta]] = { for (prev :: next :: Nil <- history.sliding(2)) yield { - for ((prev, next) <- prev.processors.zip(next.processors)) + for ((prev, next) <- prev.processors zip next.processors ) yield calculateDelta(prev.usage, next.usage) }.toList }.toList - def total(cores: List[UsageDelta]): UsageDelta = { + private def total(cores: List[UsageDelta]): UsageDelta = { cores.foldLeft(UsageDelta(0, 0, 0)) { (acc, item) => UsageDelta( acc.user + item.user, @@ -62,9 +62,10 @@ object CPU extends Module { } class Backend[T](scope: BackendScope[Timeline[T], _]) extends Listener[Timeline[T]] { - def update(timeline: Timeline[T]) = { - scope.forceUpdate() - } + + override def update(timeline: Timeline[T]): CallbackTo[Unit] = + scope.forceUpdate + } val cpuInfoStyle = Seq( @@ -73,40 +74,43 @@ object CPU extends Module { flex := "1" ) - val comp = ReactComponentB[Timeline[CPUInfo]]("CPU") + val comp = ScalaComponent.builder[Timeline[CPUInfo]]("CPU") .stateless .backend(new Backend(_)) - .render(scope => { - val graphData = deltas(scope.props.samples).map(total) - div(cpuInfoStyle)( - SVGDiagram.component( - SVGDiagram.Props( - for ((d, i) <- graphData.reverse.zipWithIndex) yield { - Point(i, d.percentActive) - }, - (data: List[Point]) => new Rectangle(0, 0, scope.props.sampleCount - 2, 1) - ) - ), - for (info <- scope.props.samples.headOption) yield { - ui.components.infoTable( - ("Model:", info.modelName), - ("Architecture:", info.archName), - ("Features:", info.features.mkString(", ")), - ("Number of Processors:", info.numOfProcessors) - ) - } + .render_P(props => { + val graphData: List[UsageDelta] = deltas(props.samples).map(total) + + div(cpuInfoStyle: _*)(pageContent(props, graphData): _*) + } ) - } - ).componentDidMount(scope => { - scope.props.addListener(scope.backend) - }) - .componentWillUnmount(scope => { - scope.props.removeListener(scope.backend) - }) + .componentDidMount(scope => scope.props.addListener(scope.backend) ) + .componentWillUnmount(scope => scope.props.removeListener(scope.backend) ) .build override val component: TagMod = comp(cpuTimeline) + def pageContent(props: Timeline[CPUInfo], graphData: List[UsageDelta]): Seq[TagMod] = { + val sVGDiagram = SVGDiagram.component( + SVGDiagram.Props( + for ((d, i) <- graphData.reverse.zipWithIndex) yield { + Point(i, d.percentActive) + }, + (_: List[Point]) => new Rectangle(0, 0, props.sampleCount - 2, 1) + ) + ) + + val tables = props.samples.headOption.map { info => + ui.components.infoTable( + ("Model:", info.modelName), + ("Architecture:", info.archName), + ("Features:", info.features.mkString(", ")), + ("Number of Processors:", info.numOfProcessors) + ) + } + + sVGDiagram :: tables.toList + } + } diff --git a/src/main/scala/monitor/modules/Display.scala b/src/main/scala/monitor/modules/Display.scala index 843fefa..d6d4d21 100644 --- a/src/main/scala/monitor/modules/Display.scala +++ b/src/main/scala/monitor/modules/Display.scala @@ -2,8 +2,10 @@ package monitor.modules import chrome.system.display.bindings.DisplayInfo import japgolly.scalajs.react.vdom.all._ -import japgolly.scalajs.react.{BackendScope, _} +import japgolly.scalajs.react.{BackendScope, Callback, CallbackTo, ScalaComponent} +import monitor.ui +import scala.concurrent.Future import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue object Display extends Module { @@ -27,7 +29,7 @@ object Display extends Module { case class Backend(scope: BackendScope[_, State]) { - def init() = { + def init(): Future[CallbackTo[Unit]] = { chrome.system.display.Display.getInfo.map(displays => { scope.modState(_.copy(displays = displays)) }) @@ -36,18 +38,14 @@ object Display extends Module { } - val comp = ReactComponentB[Unit]("Displays") + val comp = ScalaComponent.builder[Unit]("Displays") .initialState(State()) - .backend(new Backend(_)) - .render((p, s, b) => { - div(width := "100%")( - for (display <- s.displays) yield { - displayView(display) - } + .backend(Backend.apply) + .render_S( s => + div(width := "100%")(s.displays.map(displayView): _*) ) - }) - .componentWillMount((s) => s.backend.init()) - .buildU + .componentWillMount((s) => Callback.future(s.backend.init())) + .build val component: TagMod = comp() diff --git a/src/main/scala/monitor/modules/Memory.scala b/src/main/scala/monitor/modules/Memory.scala index 589ea8d..cadd50d 100644 --- a/src/main/scala/monitor/modules/Memory.scala +++ b/src/main/scala/monitor/modules/Memory.scala @@ -1,12 +1,14 @@ package monitor.modules import chrome.system.memory.bindings.MemoryInfo +import japgolly.scalajs.react.vdom.TagOf import japgolly.scalajs.react.vdom.all._ -import japgolly.scalajs.react.{BackendScope, ReactComponentB} -import monitor.Timeline +import japgolly.scalajs.react.{BackendScope, CallbackTo, ScalaComponent} import monitor.Timeline.Listener import monitor.math._ import monitor.ui.components.SVGDiagram +import monitor.{Timeline, ui} +import org.scalajs.dom.html.Table import scala.concurrent.duration._ @@ -15,55 +17,55 @@ object Memory extends Module { val name = "Memory" val iconUrl = "/assets/icons/scalable/ram.svg" - val memoryInfoStyle = Seq( + val memoryInfoStyle: Seq[TagMod] = Seq( display.flex, flexDirection := "column", flex := "1" ) class Backend[T](scope: BackendScope[Timeline[T], _]) extends Listener[Timeline[T]] { - def update(timeline: Timeline[T]) = { - scope.forceUpdate() + + def update(timeline: Timeline[T]): CallbackTo[Unit] = { + scope.forceUpdate } } val memoryTimeline = { - val timeline = new Timeline[MemoryInfo](60, 1.second)({ + val timeline = new Timeline[MemoryInfo](60, 1.second)( chrome.system.memory.Memory.getInfo - }) + ) timeline.start() - timeline } - val comp = ReactComponentB[Timeline[MemoryInfo]]("Memory") + val comp = ScalaComponent.builder[Timeline[MemoryInfo]]("Memory") .stateless .backend(new Backend(_)) - .render(scope => - div(memoryInfoStyle)( - SVGDiagram.component(SVGDiagram.Props( - for ((d, i) <- scope.props.samples.reverse.zipWithIndex) yield { - Point(i, (d.capacity - d.availableCapacity) / d.capacity) - }, - (data: List[Point]) => new Rectangle(0, 0, scope.props.sampleCount - 1, 1) - )), - for (info <- scope.props.samples.headOption) yield { - val used = (info.capacity - info.availableCapacity) / (1024 * 1014 * 1024) - val capacity = info.capacity / (1024 * 1014 * 1024) - ui.components.infoTable( - ("Memory", f"$used%3.1fGB / $capacity%3.1fGB") - ) - } - ) - ) - .componentDidMount(scope => { - scope.props.addListener(scope.backend) - }) - .componentWillUnmount(scope => { - scope.props.removeListener(scope.backend) - }) + .render_P( props => div(memoryInfoStyle: _*)( pageContent(props): _*) ) + .componentDidMount(scope => scope.props.addListener(scope.backend) ) + .componentWillUnmount(scope => scope.props.removeListener(scope.backend) ) .build override val component: TagMod = comp(memoryTimeline) + def pageContent(props: Timeline[MemoryInfo]): Seq[TagMod] = { + val svgDiagram = SVGDiagram.component(SVGDiagram.Props( + for ((d, i) <- props.samples.reverse.zipWithIndex) yield { + Point(i, (d.capacity - d.availableCapacity) / d.capacity) + }, + (_: List[Point]) => new Rectangle(0, 0, props.sampleCount - 1, 1) + )) + + + val tables: Option[TagOf[Table]] = props.samples.headOption.map { info => + val used = (info.capacity - info.availableCapacity) / (1024 * 1014 * 1024) + val capacity = info.capacity / (1024 * 1014 * 1024) + ui.components.infoTable( + ("Memory", f"$used%3.1fGB / $capacity%3.1fGB") + ) + } + + svgDiagram :: tables.toList + } + } diff --git a/src/main/scala/monitor/modules/Network.scala b/src/main/scala/monitor/modules/Network.scala index 03ba5ad..7a1d1d3 100644 --- a/src/main/scala/monitor/modules/Network.scala +++ b/src/main/scala/monitor/modules/Network.scala @@ -1,10 +1,13 @@ package monitor.modules import chrome.system.network._ +import japgolly.scalajs.react.vdom.TagOf import japgolly.scalajs.react.vdom.all._ -import japgolly.scalajs.react.{BackendScope, ReactComponentB} +import japgolly.scalajs.react.{BackendScope, Callback, CallbackTo, ScalaComponent} import org.scalajs.dom +import org.scalajs.dom.html.Div +import scala.concurrent.Future import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue object Network extends Module { @@ -16,15 +19,18 @@ object Network extends Module { case class State(interfaces: List[Interface] = List(), online: Boolean = false) case class Backend(scope: BackendScope[_, State]) { - def init() = { + def init(): Future[CallbackTo[Unit]] = { scope.modState(_.copy(online = dom.window.navigator.onLine)) - dom.window.addEventListener("online", (e: dom.Event) => { + + dom.window.addEventListener("online", (_: dom.Event) => scope.modState(_.copy(online = dom.window.navigator.onLine)) - }) - dom.window.addEventListener("offline", (e: dom.Event) => { + ) + + dom.window.addEventListener("offline", (_: dom.Event) => { scope.modState(_.copy(online = dom.window.navigator.onLine)) }) - chrome.system.network.Network.getNetworkInterfaces.foreach(ifaces => { + + chrome.system.network.Network.getNetworkInterfaces.map(ifaces => { scope.modState(_.copy(interfaces = ifaces)) }) } @@ -33,7 +39,7 @@ object Network extends Module { - def interfaceView(iface: Interface): ReactTag = { + def interfaceView(iface: Interface): TagOf[Div] = { div( div( padding := "15px 10px", @@ -49,25 +55,23 @@ object Network extends Module { boxShadow := "inset 0px 0px 10px 0px rgba(40, 40, 40, 0.75)", overflow.auto )( - for(configs <- iface.configurations) yield { + iface.configurations.map { configs => li(s"${configs.address}/${configs.prefixLength}") - } + }: _* ) ) } - val comp = ReactComponentB[Unit]("NetworkComponent") + val comp = ScalaComponent.builder[Unit]("NetworkComponent") .initialState(State()) - .backend(new Backend(_)) - .render((p, s, b) => { + .backend(Backend.apply) + .render_S( s => { div(width := "100%")( - for(iface <- s.interfaces) yield { - interfaceView(iface) - } + s.interfaces.map(interfaceView): _* ) }) - .componentWillMount((s) => s.backend.init()) - .buildU + .componentWillMount( s => Callback.future(s.backend.init()) ) + .build val component: TagMod = comp() diff --git a/src/main/scala/monitor/modules/Settings.scala b/src/main/scala/monitor/modules/Settings.scala index 8747233..d218fd6 100644 --- a/src/main/scala/monitor/modules/Settings.scala +++ b/src/main/scala/monitor/modules/Settings.scala @@ -1,6 +1,7 @@ package monitor.modules import japgolly.scalajs.react.vdom.all._ +import monitor.ui object Settings extends Module { diff --git a/src/main/scala/monitor/styles/Default.scala b/src/main/scala/monitor/styles/Default.scala index 102a291..1f24763 100644 --- a/src/main/scala/monitor/styles/Default.scala +++ b/src/main/scala/monitor/styles/Default.scala @@ -1,6 +1,6 @@ package monitor.styles -import scalacss.Defaults._ +import scalacss.DevDefaults._ object Default extends StyleSheet.Inline { diff --git a/src/main/scala/monitor/ui/components/ListView.scala b/src/main/scala/monitor/ui/components/ListView.scala index afe4857..f2ddd9b 100644 --- a/src/main/scala/monitor/ui/components/ListView.scala +++ b/src/main/scala/monitor/ui/components/ListView.scala @@ -1,43 +1,48 @@ -package ui.components +package monitor.ui.components import japgolly.scalajs.react._ +import japgolly.scalajs.react.component.builder.Lifecycle.RenderScope +import japgolly.scalajs.react.vdom.TagOf import japgolly.scalajs.react.vdom.all._ -import japgolly.scalajs.react.ReactComponentC.ReqProps +import org.scalajs.dom.html.LI object ListView { case class State[T](items: List[T], selected: Option[T] = None) - case class Props[T](items: List[T], selected: Option[T], itemView: (T, Boolean) => ReactNode, onSelect: (T) => Unit) + case class Props[T](items: List[T], selected: Option[T], itemView: (T, CallbackTo[Boolean]) => TagMod, onSelect: (T) => Unit) class Backend[T](scope: BackendScope[Props[T], State[T]]) { - def select(item: T) = { + def select(item: T): CallbackTo[Unit] = { scope.modState(_.copy(selected = Some(item))) - scope.props.onSelect(item) + scope.props.map(_.onSelect(item)) } - def isSelected(item: T): Boolean = { - scope.state.selected.exists(_ == item) + def isSelected(item: T): CallbackTo[Boolean] = { + scope.state.map(_.selected.exists(_ == item)) } } - def apply[T]() = ReactComponentB[Props[T]]("ListView") - .initialStateP(p => State(p.items, p.selected)) + def apply[T]() = ScalaComponent.builder[Props[T]]("ListView") + .initialStateFromProps(p => State(p.items, p.selected)) .backend(new Backend(_)) - .render { (p, s, b) => { + .render { lifecycle => { ul()( - for (item <- s.items) yield { - li(onClick ==> ((e: ReactEvent) => b.select(item)))( - p.itemView(item, b.isSelected(item)) - ) - } + listElements(lifecycle): _* ) } } .build + def listElements[T](lifecycle: RenderScope[Props[T], State[T], Backend[T]]): List[TagOf[LI]] = { + lifecycle.state.items.map { item => + li(onClick ==> ((_: ReactEvent) => lifecycle.backend.select(item)))( + lifecycle.props.itemView(item, lifecycle.backend.isSelected(item)) + ) + } + } } diff --git a/src/main/scala/monitor/ui/components/Loading.scala b/src/main/scala/monitor/ui/components/Loading.scala index 7eecdd9..ca52a98 100644 --- a/src/main/scala/monitor/ui/components/Loading.scala +++ b/src/main/scala/monitor/ui/components/Loading.scala @@ -1,7 +1,7 @@ -package ui.components +package monitor.ui.components -import japgolly.scalajs.react.ReactComponentB -import japgolly.scalajs.react.vdom.ReactTag +import japgolly.scalajs.react.ScalaComponent +import japgolly.scalajs.react.vdom.VdomElement import japgolly.scalajs.react.vdom.all._ import scala.concurrent.Future @@ -10,13 +10,13 @@ import scala.util.{Failure, Success} object Loading { - case class Props(future: Future[ReactTag], loading: ReactTag) + case class Props(future: Future[VdomElement], loading: VdomElement) - def loading = ReactComponentB[Props]("AsyncComponent") - .render(props => { + def loading = ScalaComponent.builder[Props]("AsyncComponent") + .render_P(props => { props.future.value match { - case Some(Success(value)) => value - case Some(Failure(error)) => div("failed") + case Some(Success(vdomElement)) => vdomElement + case Some(Failure(_)) => div("failed") case None => props.loading } }) diff --git a/src/main/scala/monitor/ui/components/SVGDiagram.scala b/src/main/scala/monitor/ui/components/SVGDiagram.scala index d8ef53a..b908112 100644 --- a/src/main/scala/monitor/ui/components/SVGDiagram.scala +++ b/src/main/scala/monitor/ui/components/SVGDiagram.scala @@ -1,8 +1,12 @@ package monitor.ui.components -import japgolly.scalajs.react.ReactComponentB -import japgolly.scalajs.react.vdom.all._ -import monitor.math._ +import japgolly.scalajs.react.ScalaComponent +import japgolly.scalajs.react.vdom.Implicits.{vdomAttrVtKey, vdomAttrVtString} +import japgolly.scalajs.react.vdom.all.{backgroundColor, borderRadius, flex, svg} +import japgolly.scalajs.react.vdom.{TagMod, TagOf, VdomElement} +import monitor.math.{Point, Rectangle} +import org.scalajs.dom.svg.G + object SVGDiagram { @@ -13,25 +17,25 @@ object SVGDiagram { trait Style { - import svg._ + import svg.{shapeRendering, stroke, strokeOpacity, strokeWidth, vectorEffect} - def gridLine: TagMod = Seq( + def gridLine: Seq[TagMod] = Seq( vectorEffect.nonScalingStroke, strokeWidth := "2px" ) - def grid: TagMod = Seq( + def grid: Seq[TagMod] = Seq( strokeOpacity := 0.1, stroke := "#2A2A2A" ) - def path: TagMod = Seq( + def path: Seq[TagMod] = Seq( vectorEffect.nonScalingStroke, strokeWidth := "2px" ) - def root: TagMod = Seq( - flex := 1, + def root: Seq[TagMod] = Seq( + flex := "1", backgroundColor := "#ffffff", shapeRendering := "geometricPrecision", borderRadius := "5px" @@ -60,36 +64,35 @@ object SVGDiagram { // ) //} - def grid(p: Props, bounds: Rectangle): ReactTag = { + def grid(p: Props, bounds: Rectangle): TagOf[G] = { import svg._ - g(p.style.grid)( - for (index <- 0 to 5) yield { - line(p.style.gridLine)(x1 := 0, y1 := index / 5.0, x2 := bounds.max.x, y2 := index / 5.0) - } + g(p.style.grid: _*)( + (0 to 5).map { index => line(p.style.gridLine: _*)(x1 := 0, y1 := index / 5.0, x2 := bounds.max.x, y2 := index / 5.0) }: _* ) } - def lineChart(p: Props): ReactTag = { + def lineChart(p: Props): VdomElement = { + import svg.{d, fill, fillOpacity, g, height, path, preserveAspectRatio, stroke, strokeWidth, transform, viewBox, width} + val bounds = p.view(p.data) - import svg._ + val pathData = p.data.foldLeft(new StringBuilder("M0 0")) { case (acc, data) => acc.append(s" ${data.x} ${data.y}") }.append(s"V0 ").toString - svg(p.style.root)(width := "100%", height := "100%", viewBox := s"0 0 ${bounds.max.x} ${bounds.max.y}", preserveAspectRatio := "none")( + svg.svg(p.style.root: _*)(width := "100%", height := "100%", viewBox := s"0 0 ${bounds.max.x} ${bounds.max.y}", preserveAspectRatio := "none")( g(transform := "translate(0, 1)", fill := "black", stroke := "#bada55", strokeWidth := 0.01)( g(transform := "scale(1,-1)", fill := "#bada55", fillOpacity := 0.3)( - path(p.style.path)(d := pathData), + path(p.style.path: _*)(d := pathData), grid(p, bounds) ) ) ) - } - val component = ReactComponentB[Props]("SVGDiagram") - .render { p => - lineChart(p) } + + val component = ScalaComponent.builder[Props]("SVGDiagram") + .render_P(lineChart) .build } diff --git a/src/main/scala/monitor/ui/components/package.scala b/src/main/scala/monitor/ui/components/package.scala index faf1d9c..0c86dba 100644 --- a/src/main/scala/monitor/ui/components/package.scala +++ b/src/main/scala/monitor/ui/components/package.scala @@ -1,4 +1,4 @@ -package ui +package monitor.ui import japgolly.scalajs.react.vdom.all._ import monitor.styles.Default @@ -15,13 +15,13 @@ package object components { ) ++ firstRowStyle table(width := "100%", borderCollapse.collapse)( tbody( - for ((row, index) <- content.zipWithIndex) yield { + content.zipWithIndex.map { case (row, index) => val style = if (index == 0) firstRowStyle else otherRowsStyle tr()( - td(style)(row._1), - td(style)(row._2) + td(style: _*)(row._1), + td(style: _*)(row._2) ) - } + }: _* ) ) }