diff --git a/Readme.md b/Readme.md
index 8ce971ba..d6afb51f 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,3 +1,6 @@
+Note: for Table Of Contents, just click the burger icon top right of this document, just above this text on the right.
+
+![artifact](https://img.shields.io/maven-central/v/io.github.kostaskougios/terminal21-server_3)
# Terminal 21
Terminal 21 is a library and server that give scala command line programs (i.e. scala-cli scripts) the ability to easily
@@ -6,15 +9,61 @@ of what can be done at the [terminal 21 youtube channel](https://www.youtube.com
For scala 3 and jdk21 or better. If you have scala-cli installed, you won't need to download scala 3 or jdk21, see below for instructions on how to quickly start with terminal21.
-Terminal21 consist of :
-- a web server that can be easily deployed on your laptop, home network etc
-- scala apps (scala-cli/ammonite scripts or just normal scala apps) that use the terminal21 UI libs to create user interfaces
+Note: feel free to ask questions in the "Discussions" board at the top of this github page.
-The terminal21 libs have a websocket open with the server, and they send / receive instructions and events. Similarly, the server
-has a websocket open with the React frontend to do the same. Events like clicks or changes to input boxes instantly update
-the state in the client scripts.
+# Quick start with terminal21
-The best and easiest way to start with terminal 21 is via scala-cli and a simple example.
+The easiest way to start with terminal21 is to clone this repository. There is a scala-cli
+script that starts the server (all deps and jdk21 will be downloaded automatically by scala-cli).
+
+```shell
+git clone https://github.com/kostaskougios/terminal21-restapi.git
+cd terminal21-restapi/example-scripts
+
+# start the server
+./server.sc
+# ... it will download dependencies & jdk and start the server.
+```
+Now open your browser to http://localhost:8080/ui/ . You'll have the terminal21 UI, will be a bit empty for now, just the settings tab. But we will shortly run some scripts with UI's.
+
+Let's run some example scripts. All scripts use [project.scala](example-scripts/project.scala) with some common settings and dependencies.
+
+Start with a hello world example:
+
+[hello-world.sc](example-scripts/hello-world.sc)
+
+Run it
+```shell
+./hello-world.sc
+```
+
+and check your browser. Changes in the terminal21 UI will reflect instantly.
+
+And then continue with a more complicated csv editor:
+
+[csv-editor.sc](example-scripts/csv-editor.sc) : edit csv files.
+
+```shell
+./csv-editor.sc -- /tmp/wargame.csv
+```
+(note the "--": this is a scala-cli parameter needed before passing actual arguments to a script. The actual argument is the csv filename.)
+
+Terminal21 UI will now have the csv editor's tab:
+![csv editor](docs/images/csv-editor.png)
+
+If we click in a cell, we will be able to change a value. And then use the "Save & Exit" button to save the file and exit.
+
+![csv editor](docs/images/csv-editor-change.png)
+
+Now feel free to examine and run the rest of the scripts or create your own! You can have the server running and develop your
+scripts with your favorite IDE. Run the scripts within the IDE and view the UI in a browser.
+
+# Example scripts
+
+```shell
+ls *.sc
+bouncing-ball.sc csv-editor.sc csv-viewer.sc hello-world.sc mathjax.sc nivo-line-chart.sc postit.sc server.sc textedit.sc
+```
Let's create a simple hello world script in scala-cli that uses terminal21 server to render the UI.
@@ -56,47 +105,86 @@ can be used for things like:
- even small web based games, maybe starting with [bouncing-ball.sc](example-scripts/bouncing-ball.sc)
- POC code at the office can be presented via a scala-cli script + terminal21 UI. The POC code can be imported as a lib in a script.
- logs can be viewed and searched via scripts
-- ... and so on
-# Quick start with terminal21
+![notebooks, spark notebooks and maths](docs/images/nivo/responsiveline.png)
+![notebooks, spark notebooks and maths](docs/images/mathjax/mathjaxbig.png)
-The easiest way to start with terminal21 is to clone this repository. There is a scala-cli
-script that starts the server (all deps and jdk21 will be downloaded automatically by scala-cli).
+- notebooks with charts like [notebooks](example-scripts/nivo-line-chart.sc) and with maths like [maths](example-scripts/mathjax.sc)
+- spark notebooks like [spark-notebook.sc](example-spark/spark-notebook.sc)
-```shell
-git clone https://github.com/kostaskougios/terminal21-restapi.git
-cd terminal21-restapi/example-scripts
+# Available UI Components
-# start the server
-./server.sc
-# ... it will download dependencies & jdk and start the server.
-```
-Now open your browser to http://localhost:8080/ui/ . You'll have the terminal21 UI, will be a bit empty for now, just the settings tab. But we will shortly run some scripts with UI's.
+Standard html elements
+[Std](docs/std.md)
-Let's run some example scripts. All scripts use project.scala with some common settings and dependencies.
+Generic components for buttons, menus, forms, text, grids, tables:
+[Chakra](docs/chakra.md)
-[csv-editor.sc](example-scripts/csv-editor.sc) : edit csv files.
+Charts and visualisation:
+[Nivo](docs/nivo.md)
-```shell
-./csv-editor.sc -- /tmp/wargame.csv
-```
-(note the "--": this is a scala-cli parameter needed before passing actual arguments to a script. The actual argument is the csv filename.)
+Maths:
+[MathJax](docs/mathjax.md)
-Terminal21 UI will now have the csv editor's tab:
-![csv editor](docs/images/csv-editor.png)
+Spark:
+[Spark](docs/spark.md)
+# Architecture
-If we click in a cell, we will be able to change a value. And then use the "Save & Exit" button to save the file and exit.
+Terminal21 consist of :
+- a scala/react based web server that can be easily deployed on your laptop, home network etc
+- scala apps (scala-cli/ammonite scripts or just normal scala apps) that use the terminal21 UI libs to create user interfaces
-![csv editor](docs/images/csv-editor-change.png)
+The terminal21 libs have a websocket open with the server, and they send / receive instructions and events. Similarly, the server
+has a websocket open with the React frontend on the browser to do the same. Events like clicks or changes to input boxes instantly update
+the state in the client scripts.
-Now feel free to examine and run the rest of the scripts or create your own! I found out MS code works better for scala-cli scripts but
-please make sure you include the terminal21 libs in the script rather than in `project.scala`. This will make autocomplete work better.
+![architecture](docs/images/terminal21-architecture.png)
-```shell
-ls *.sc
-bouncing-ball.sc csv-editor.sc csv-viewer.sc hello-world.sc postit.sc server.sc textedit.sc
+# Mutability
+
+terminal21 ui components are mutable. This is a decision choice (for now) because of how much more simple code is this way. I.e.
+changing the text of a paragraph on an event handler is as simple as :
+
+```scala
+ p.text = "new text"
```
+The equivalent immutable code would be (at least)
+```scala
+ p.copy(text= "new text")
+```
+
+Also by default some component values (like input boxes) are changed by the user. These changes are reflected in the component graph, something that
+would be a lot harder if the graph was immutable.
+
+If there is a reasonable way to refactor to have immutability without compromising simplicity, it will be done.
+
# Need help?
-Please use the discussions of the project to post any questions, comments or ideas.
+Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi/discussions) of the project to post any questions, comments or ideas.
+
+# Changelog
+
+## Version 0.11
+
+- Quick* classes to simplify UI creation
+- A few Nivo visualisation components
+- spark integration, terminal21 acts as a spark notebook
+- ui component documentation
+
+## Version 0.1
+
+- initial release with std and chakra components
+
+
+# Our thanks
+
+![yourkit](https://www.yourkit.com/images/yklogo.png)
+
+To yourkit for their excellent profiler.
+
+YourKit supports open source projects with innovative and intelligent tools
+for monitoring and profiling Java and .NET applications.
+YourKit is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler/),
+[YourKit .NET Profiler](https://www.yourkit.com/dotnet-profiler/),
+and [YourKit YouMonitor](https://www.yourkit.com/youmonitor/).
\ No newline at end of file
diff --git a/bin/publish-local b/bin/publish-local
index 3ab1bc07..6bfcc82b 100755
--- a/bin/publish-local
+++ b/bin/publish-local
@@ -4,4 +4,4 @@ cd ../terminal21-ui
bin/build-and-copy-to-restapi
cd ../terminal21-restapi
-sbt clean compile publishLocal
+sbt clean terminal21-server-client-common/publishLocal terminal21-ui-std-exports/publishLocal compile publishLocal
diff --git a/bin/publish-maven-central-signed b/bin/publish-maven-central-signed
new file mode 100755
index 00000000..d163eb01
--- /dev/null
+++ b/bin/publish-maven-central-signed
@@ -0,0 +1,8 @@
+#! /bin/sh
+
+cd ../terminal21-ui
+bin/build-and-copy-to-restapi
+
+cd ../terminal21-restapi
+sbt clean terminal21-server-client-common/publishLocal terminal21-ui-std-exports/publishLocal compile publishSigned
+
diff --git a/build.sbt b/build.sbt
index 5f266381..f9bf9122 100644
--- a/build.sbt
+++ b/build.sbt
@@ -3,7 +3,7 @@
*/
val scala3Version = "3.3.1"
-ThisBuild / version := "0.1"
+ThisBuild / version := "0.11"
ThisBuild / organization := "io.github.kostaskougios"
name := "rest-api"
ThisBuild / scalaVersion := scala3Version
@@ -43,8 +43,15 @@ val HelidonClientWebSocket = "io.helidon.webclient" % "helidon-webclient-websock
val HelidonClient = "io.helidon.webclient" % "helidon-webclient-http2" % HelidonVersion
val HelidonServerLogging = "io.helidon.logging" % "helidon-logging-jul" % HelidonVersion
-val LogBack = "ch.qos.logback" % "logback-classic" % "1.4.14"
-val Slf4jApi = "org.slf4j" % "slf4j-api" % "2.0.9"
+val LogBack = "ch.qos.logback" % "logback-classic" % "1.4.14"
+val Slf4jApi = "org.slf4j" % "slf4j-api" % "2.0.9"
+
+val SparkSql = ("org.apache.spark" %% "spark-sql" % "3.5.0" % "provided").cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
+val SparkScala3Fix = Seq(
+ "io.github.vincenzobaz" %% "spark-scala3-encoders" % "0.2.5",
+ "io.github.vincenzobaz" %% "spark-scala3-udf" % "0.2.5"
+).map(_.exclude("org.scala-lang.modules", "scala-xml_2.13"))
+
// -----------------------------------------------------------------------------------------------
// Modules
// -----------------------------------------------------------------------------------------------
@@ -132,9 +139,40 @@ lazy val `terminal21-ui-std` = project
)
.enablePlugins(FunctionsRemotePlugin)
-lazy val examples = project
+lazy val `end-to-end-tests` = project
.settings(
commonSettings,
libraryDependencies ++= Seq(ScalaTest, LogBack)
)
- .dependsOn(`terminal21-ui-std`)
+ .dependsOn(`terminal21-ui-std`, `terminal21-nivo`, `terminal21-mathjax`)
+
+lazy val `terminal21-nivo` = project
+ .settings(
+ commonSettings,
+ libraryDependencies ++= Seq(
+ ScalaTest
+ )
+ )
+ .dependsOn(`terminal21-ui-std` % "compile->compile;test->test")
+
+lazy val `terminal21-spark` = project
+ .settings(
+ commonSettings,
+ Test / fork := true,
+ Test / javaOptions ++= Seq("--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED"),
+ libraryDependencies ++= Seq(
+ ScalaTest,
+ SparkSql,
+ LogBack % Test
+ ) ++ SparkScala3Fix
+ )
+ .dependsOn(`terminal21-ui-std` % "compile->compile;test->test", `terminal21-nivo` % Test)
+
+lazy val `terminal21-mathjax` = project
+ .settings(
+ commonSettings,
+ libraryDependencies ++= Seq(
+ ScalaTest
+ )
+ )
+ .dependsOn(`terminal21-ui-std` % "compile->compile;test->test")
diff --git a/docs/chakra.md b/docs/chakra.md
new file mode 100644
index 00000000..db5f3d92
--- /dev/null
+++ b/docs/chakra.md
@@ -0,0 +1,269 @@
+# Chakra components
+
+Chakra components build on top of standard html elements to provide nice UI for text, forms, tables etc.
+See [Chakra Components for React](https://chakra-ui.com/docs/components).
+
+These case classes can be used to render chakra components.
+[The case classes](../terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala)
+
+Note: not all chakra elements are available and also only a fraction of the available properties of each component is documented here.
+See the case class for a full list as well as the chakra react documentation (links are available in the scaladocs of each case class).
+
+If you want a chakra component that is not supported yet, please add a comment here: [Chakra support discussion](https://github.com/kostaskougios/terminal21-restapi/discussions/2)
+
+[Examples](https://github.com/kostaskougios/terminal21-restapi/tree/main/example-scripts)
+
+Dependency: `io.github.kostaskougios::terminal21-ui-std:$VERSION`
+
+### Text
+
+```scala
+Text(text = "typography-text-0001", color = Some("tomato"))
+```
+
+![Text](images/chakra/text.png)
+
+### Button
+
+![Button](images/chakra/button.png)
+```scala
+val b = Button(text = "Keep Running")
+b.onClick: () =>
+ b.text = "Clicked"
+ session.render()
+```
+
+### Box
+
+![Box](images/chakra/box.png)
+```scala
+Box(text = "Badges", bg = "green", p = 4, color = "black")
+```
+
+## HStack / VStack
+Horizontal / Vertical stack of elements
+
+![HStack](images/chakra/hstack.png)
+```scala
+HStack().withChildren(
+ checkbox1,
+ checkbox2
+)
+```
+
+### Menus
+
+![Menu](images/chakra/menu.png)
+
+```scala
+Menu().withChildren(
+ MenuButton(text = "Actions menu0001", size = Some("sm"), colorScheme = Some("teal")).withChildren(
+ ChevronDownIcon()
+ ),
+ MenuList().withChildren(
+ MenuItem(text = "Download menu-download")
+ .onClick: () =>
+ box1.text = "'Download' clicked"
+ session.render()
+ ,
+ MenuItem(text = "Copy").onClick: () =>
+ box1.text = "'Copy' clicked"
+ session.render()
+ )
+)
+```
+
+### Forms
+
+see [Forms](../end-to-end-tests/src/main/scala/tests/chakra/Forms.scala) as an example on how to create forms.
+
+![Forms](images/chakra/forms.png)
+
+Use FormControl to wrap your form elements:
+
+```scala
+FormControl().withChildren(
+ FormLabel(text = "Email address"),
+ InputGroup().withChildren(
+ InputLeftAddon().withChildren(EmailIcon()),
+ Input(`type` = "email", value = "my@email.com"),
+ InputRightAddon().withChildren(CheckCircleIcon(color = Some("green")))
+ ),
+ FormHelperText(text = "We'll never share your email.")
+)
+```
+
+### Editable
+
+Editable is a textarea but looks like normal text until the user clicks it.
+
+```scala
+val editable1 = Editable(defaultValue = "Please type here").withChildren(
+ EditablePreview(),
+ EditableInput()
+)
+
+editable1.onChange: newValue =>
+ status.text = s"editable1 newValue = $newValue, verify editable1.value = ${editable1.value}"
+ session.render()
+
+// Use text area for longer texts
+val editable2 = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren(
+ EditablePreview(),
+ EditableTextarea()
+)
+
+```
+![Editable](images/chakra/editable.png)
+![Editable](images/chakra/editable-editing.png)
+
+### SimpleGrid
+
+```scala
+SimpleGrid(spacing = Some("8px"), columns = 4).withChildren(
+ Box(text = "One", bg = "yellow", color = "black"),
+ Box(text = "Two", bg = "tomato", color = "black"),
+ Box(text = "Three", bg = "blue", color = "black")
+)
+```
+
+![SimpleGrid](images/chakra/simplegrid.png)
+
+### Center / Circle / Square
+```scala
+Center(text = "Center demo, not styled"),
+Center(text = "Center demo center-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
+Circle(text = "Circle demo, not styled"),
+Circle(text = "Circle demo circle-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
+Square(text = "Square demo, not styled"),
+Square(text = "Square demo square-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px"))
+```
+
+![CCS](images/chakra/ccs.png)
+
+### Image
+
+```scala
+Image(
+ src = "https://bit.ly/dan-abramov",
+ alt = "Dan Abramov",
+ boxSize = Some("150px")
+),
+Image(
+ src = "https://bit.ly/dan-abramov",
+ alt = "Dan Abramov",
+ boxSize = Some("150px"),
+ borderRadius = Some("full")
+)
+```
+
+![Images](images/chakra/images.png)
+
+Images can be hosted in the server under ~/.terminal21:
+
+```shell
+ ~/.terminal21 tree
+.
+└── web
+ └── images
+ ├── ball.png
+ ├── logo1.png
+ └── logo2.png
+```
+and then used like:
+```scala
+Image(
+ src = "/web/images/logo1.png",
+ alt = "logo no 1",
+ boxSize = Some("150px"),
+ borderRadius = Some("full")
+)
+```
+
+### Icons
+
+see [Chakra Icons](https://chakra-ui.com/docs/components/icon).
+
+```scala
+InfoIcon(color = Some("tomato")),
+MoonIcon(color = Some("green")),
+AddIcon(),
+ArrowBackIcon(),
+ArrowDownIcon(),
+ArrowForwardIcon(),
+ArrowLeftIcon(),
+ArrowRightIcon(),
+ArrowUpIcon(),
+ArrowUpDownIcon(),
+AtSignIcon(),
+AttachmentIcon(),
+BellIcon(),
+CalendarIcon(),
+ChatIcon(),
+CheckIcon(),
+CheckCircleIcon(),
+ChevronDownIcon(),
+ChevronLeftIcon(),
+ChevronRightIcon(),
+ChevronUpIcon(),
+CloseIcon(),
+CopyIcon(),
+DeleteIcon(),
+DownloadIcon(),
+DragHandleIcon(),
+EditIcon(),
+EmailIcon(),
+ExternalLinkIcon(),
+HamburgerIcon(),
+InfoIcon(),
+InfoOutlineIcon(),
+LinkIcon(),
+LockIcon(),
+MinusIcon(),
+MoonIcon(),
+NotAllowedIcon(),
+PhoneIcon(),
+PlusSquareIcon(),
+QuestionIcon(),
+QuestionOutlineIcon(),
+RepeatIcon(),
+RepeatClockIcon(),
+SearchIcon(),
+Search2Icon(),
+SettingsIcon(),
+SmallAddIcon(),
+SmallCloseIcon(),
+SpinnerIcon(),
+StarIcon(),
+SunIcon(),
+TimeIcon(),
+TriangleDownIcon(),
+TriangleUpIcon(),
+UnlockIcon(),
+UpDownIcon(),
+ViewIcon(),
+ViewOffIcon(),
+WarningIcon(),
+WarningTwoIcon()
+```
+
+![Icons](images/chakra/icons.png)
+
+### Tables
+
+There are a lot of elements to create a table but `QuickTable` component also helps simplify things. First lets see the code
+for a table [here](../end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala)
+
+![Table](images/chakra/table.png)
+
+`Quicktable` helps creating all those table elements for the most common usecases:
+
+```scala
+val conversionTable = QuickTable().headers("To convert", "into", "multiply by")
+ .caption("Imperial to metric conversion factors")
+val tableRows:Seq[Seq[String]] = Seq(
+ Seq("inches","millimetres (mm)","25.4"),
+ ...
+)
+conversionTable.rows(tableRows)
+```
diff --git a/docs/images/chakra/box.png b/docs/images/chakra/box.png
new file mode 100644
index 00000000..2178f775
Binary files /dev/null and b/docs/images/chakra/box.png differ
diff --git a/docs/images/chakra/button.png b/docs/images/chakra/button.png
new file mode 100644
index 00000000..0dff59da
Binary files /dev/null and b/docs/images/chakra/button.png differ
diff --git a/docs/images/chakra/ccs.png b/docs/images/chakra/ccs.png
new file mode 100644
index 00000000..ea254195
Binary files /dev/null and b/docs/images/chakra/ccs.png differ
diff --git a/docs/images/chakra/editable-editing.png b/docs/images/chakra/editable-editing.png
new file mode 100644
index 00000000..f7071716
Binary files /dev/null and b/docs/images/chakra/editable-editing.png differ
diff --git a/docs/images/chakra/editable.png b/docs/images/chakra/editable.png
new file mode 100644
index 00000000..6a7c64eb
Binary files /dev/null and b/docs/images/chakra/editable.png differ
diff --git a/docs/images/chakra/forms.png b/docs/images/chakra/forms.png
new file mode 100644
index 00000000..00fc91fb
Binary files /dev/null and b/docs/images/chakra/forms.png differ
diff --git a/docs/images/chakra/hstack.png b/docs/images/chakra/hstack.png
new file mode 100644
index 00000000..8d284e2b
Binary files /dev/null and b/docs/images/chakra/hstack.png differ
diff --git a/docs/images/chakra/icons.png b/docs/images/chakra/icons.png
new file mode 100644
index 00000000..35bd3616
Binary files /dev/null and b/docs/images/chakra/icons.png differ
diff --git a/docs/images/chakra/images.png b/docs/images/chakra/images.png
new file mode 100644
index 00000000..e91bf154
Binary files /dev/null and b/docs/images/chakra/images.png differ
diff --git a/docs/images/chakra/menu.png b/docs/images/chakra/menu.png
new file mode 100644
index 00000000..8b683d94
Binary files /dev/null and b/docs/images/chakra/menu.png differ
diff --git a/docs/images/chakra/simplegrid.png b/docs/images/chakra/simplegrid.png
new file mode 100644
index 00000000..da3fd244
Binary files /dev/null and b/docs/images/chakra/simplegrid.png differ
diff --git a/docs/images/chakra/table.png b/docs/images/chakra/table.png
new file mode 100644
index 00000000..63d1bcb8
Binary files /dev/null and b/docs/images/chakra/table.png differ
diff --git a/docs/images/chakra/text.png b/docs/images/chakra/text.png
new file mode 100644
index 00000000..470e3058
Binary files /dev/null and b/docs/images/chakra/text.png differ
diff --git a/docs/images/mathjax/mathjax.png b/docs/images/mathjax/mathjax.png
new file mode 100644
index 00000000..08a82537
Binary files /dev/null and b/docs/images/mathjax/mathjax.png differ
diff --git a/docs/images/mathjax/mathjaxbig.png b/docs/images/mathjax/mathjaxbig.png
new file mode 100644
index 00000000..940c12a3
Binary files /dev/null and b/docs/images/mathjax/mathjaxbig.png differ
diff --git a/docs/images/nivo/responsivebar.png b/docs/images/nivo/responsivebar.png
new file mode 100644
index 00000000..971feaf1
Binary files /dev/null and b/docs/images/nivo/responsivebar.png differ
diff --git a/docs/images/nivo/responsiveline.png b/docs/images/nivo/responsiveline.png
new file mode 100644
index 00000000..594107a4
Binary files /dev/null and b/docs/images/nivo/responsiveline.png differ
diff --git a/docs/images/spark/spark-notebook.png b/docs/images/spark/spark-notebook.png
new file mode 100644
index 00000000..e70e7913
Binary files /dev/null and b/docs/images/spark/spark-notebook.png differ
diff --git a/docs/images/spark/sparkbasics.png b/docs/images/spark/sparkbasics.png
new file mode 100644
index 00000000..878fc768
Binary files /dev/null and b/docs/images/spark/sparkbasics.png differ
diff --git a/docs/images/terminal21-architecture.png b/docs/images/terminal21-architecture.png
new file mode 100644
index 00000000..47e1b3d5
Binary files /dev/null and b/docs/images/terminal21-architecture.png differ
diff --git a/docs/mathjax.md b/docs/mathjax.md
new file mode 100644
index 00000000..d43d4ea5
--- /dev/null
+++ b/docs/mathjax.md
@@ -0,0 +1,21 @@
+# mathjax
+![MathJax](images/mathjax/mathjaxbig.png)
+
+[mathjax](https://docs.mathjax.org/en/latest/), "MathJax is an open-source JavaScript display engine for LaTeX, MathML, and AsciiMath notation ".
+
+terminal21 supports asciimath notation for mathjax version 3.
+
+Dependency: `io.github.kostaskougios::terminal21-mathjax:$VERSION`
+
+### MathJax
+
+Example: [mathjax.sc](../example-scripts/mathjax.sc) [MathJax](../end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala)
+
+```scala
+MathJax(
+ expression = """Everyone knows this one : \(ax^2 + bx + c = 0\)"""
+)
+```
+![MathJax](images/mathjax/mathjax.png)
+
+see [Writing Mathematics for MathJax](https://docs.mathjax.org/en/latest/basic/mathematics.html)
\ No newline at end of file
diff --git a/docs/nivo.md b/docs/nivo.md
new file mode 100644
index 00000000..b20ff6ec
--- /dev/null
+++ b/docs/nivo.md
@@ -0,0 +1,21 @@
+# Nivo
+
+[Nivo](https://nivo.rocks/), "nivo provides a rich set of dataviz components, built on top of D3 and React".
+
+terminal21 supports a few of these, but please add a comment [here](https://github.com/kostaskougios/terminal21-restapi/discussions/3) if you
+would like support for a particular component.
+
+Dependency: `io.github.kostaskougios::terminal21-nivo:$VERSION`
+
+### ResponsiveLine
+
+Code: [ResponsiveLine](../end-to-end-tests/src/main/scala/tests/nivo/ResponsiveLineChart.scala)
+
+![RL](images/nivo/responsiveline.png)
+
+### ResponsiveBar
+
+Code: [ResponsiveBar](../end-to-end-tests/src/main/scala/tests/nivo/ResponsiveBarChart.scala)
+
+![RB](images/nivo/responsivebar.png)
+
diff --git a/docs/spark.md b/docs/spark.md
new file mode 100644
index 00000000..daa92dc7
--- /dev/null
+++ b/docs/spark.md
@@ -0,0 +1,43 @@
+# Spark
+
+Dependency: `io.github.kostaskougios::terminal21-spark:$VERSION`
+
+Terminal 21 spark integration allows using datasets and dataframes inside terminal21 scripts/code.
+It also provides caching of datasets in order for scripts to be used as notebooks. The caching
+has also a UI component to allow invalidating the cache and re-evaluating the datasets.
+
+To give it a go, please checkout this repo and try the examples. Only requirement to do this is that you have `scala-cli` installed:
+
+```shell
+git clone https://github.com/kostaskougios/terminal21-restapi.git
+cd terminal21-restapi/example-scripts
+
+# start the server
+./server.sc
+# ... it will download dependencies & jdk and start the server.
+
+# Now lets run a spark notebook
+
+cd terminal21-restapi/example-spark
+./spark-notebook.sc
+```
+
+Leave `spark-notebook.sc` running and edit it with your preferred editor. When you save your changes, it will automatically be rerun and
+the changes will be reflected in the UI.
+
+## Using terminal21 as notebook within an ide
+
+Create a scala project (i.e. using sbt), add the terminal21 dependencies and run the terminal21 server. Create your notebook code, i.e.
+see [SparkBasics](../terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala). Run it. Let it run while
+you interact with the UI. Change the code and rerun it. Click "Recalculate" if you want the datasets to be re-evaluated.
+
+![SparkBasics](images/spark/sparkbasics.png)
+
+## Using terminal21 as notebook with scala-cli
+
+See [spark-notebook.sc](../example-spark/spark-notebook.sc).
+On top of the file, `scala-cli` is configured to run with the `--restart` option. This will terminate and restart the script
+whenever a change in the file is detected. Edit the script with your favorite IDE and when saving it, it will automatically
+re-run. If you want to re-evaluate the datasets, click "Recalculate" on the UI components.
+
+![SparkNotebook](images/spark/spark-notebook.png)
\ No newline at end of file
diff --git a/docs/std.md b/docs/std.md
new file mode 100644
index 00000000..60577914
--- /dev/null
+++ b/docs/std.md
@@ -0,0 +1,35 @@
+# Std
+
+These are standard html elements but please prefer the more flexible chakra component if it exists.
+
+[Example](../end-to-end-tests/src/main/scala/tests/StdComponents.scala)
+
+Dependency: `io.github.kostaskougios::terminal21-ui-std:$VERSION`
+
+### Paragraph, NewLine, Span, Em
+
+```scala
+Paragraph(text = "Hello World!").withChildren(
+ NewLine(),
+ Span(text = "Some more text"),
+ Em(text = " emphasized!"),
+ NewLine(),
+ Span(text = "And the last line")
+)
+```
+### Header
+
+```scala
+Header1(text = "Welcome to the std components demo/test")
+```
+
+### Input
+
+```scala
+ val input = Input(defaultValue = "Please enter your name")
+ val output = Paragraph(text = "This will reflect what you type in the input")
+ input.onChange: newValue =>
+ output.text = newValue
+ session.render()
+
+```
\ No newline at end of file
diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala
new file mode 100644
index 00000000..5940e29e
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala
@@ -0,0 +1,37 @@
+package tests
+
+import org.terminal21.client.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.{Paragraph, render}
+import tests.chakra.*
+
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.atomic.AtomicBoolean
+
+@main def chakraComponents(): Unit =
+ val keepRunning = new AtomicBoolean(true)
+
+ while keepRunning.get() do
+ println("Starting new session")
+ Sessions.withNewSession("chakra-components", "Chakra Components"): session =>
+ keepRunning.set(false)
+ given ConnectedSession = session
+
+ val latch = new CountDownLatch(1)
+
+ // react tests reset the session to clear state
+ val krButton = Button(text = "Keep Running").onClick: () =>
+ keepRunning.set(true)
+ latch.countDown()
+
+ (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components(
+ latch
+ ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Seq(krButton))
+ .render()
+
+ println("Waiting for button to be pressed for 1 hour")
+ session.waitTillUserClosesSessionOr(latch.getCount == 0)
+ if !session.isClosed then
+ session.clear()
+ Paragraph(text = "Terminated").render()
+ Thread.sleep(1000)
diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala
new file mode 100644
index 00000000..c34fe57d
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala
@@ -0,0 +1,22 @@
+package tests
+
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.mathjax.*
+
+@main def mathJaxComponents(): Unit =
+ Sessions.withNewSession("mathjax-components", "MathJax Components", MathJaxLib): session =>
+ given ConnectedSession = session
+ Seq(
+ HStack().withChildren(
+ Text(text = "Lets write some math expressions that will wow everybody!"),
+ MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""")
+ ),
+ MathJax(expression = """Everyone knows this one : \(ax^2 + bx + c = 0\). But how about this? \(\sum_{i=1}^n i^3 = ((n(n+1))/2)^2 \)"""),
+ MathJax(
+ expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""",
+ style = Map("backgroundColor" -> "gray")
+ )
+ ).render()
+ session.waitTillUserClosesSession()
diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala
new file mode 100644
index 00000000..2eb2aaa6
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala
@@ -0,0 +1,12 @@
+package tests
+
+import org.terminal21.client.components.nivo.*
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart}
+
+@main def nivoComponents(): Unit =
+ Sessions.withNewSession("nivo-components", "Nivo Components", NivoLib): session =>
+ given ConnectedSession = session
+ (ResponsiveBarChart() ++ ResponsiveLineChart()).render()
+ session.waitTillUserClosesSession()
diff --git a/examples/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala
similarity index 89%
rename from examples/src/main/scala/tests/StdComponents.scala
rename to end-to-end-tests/src/main/scala/tests/StdComponents.scala
index d0c0447b..7f633f7b 100644
--- a/examples/src/main/scala/tests/StdComponents.scala
+++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala
@@ -28,6 +28,4 @@ import org.terminal21.client.components.*
output
).render()
- for i <- 1 to 400 do
- println(s"i = $i, input value = ${input.value}")
- Thread.sleep(1000)
+ session.waitTillUserClosesSession()
diff --git a/examples/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Buttons.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala
diff --git a/examples/src/main/scala/tests/chakra/Common.scala b/end-to-end-tests/src/main/scala/tests/chakra/Common.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Common.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Common.scala
diff --git a/examples/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
similarity index 90%
rename from examples/src/main/scala/tests/chakra/DataDisplay.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
index ff7ceedb..4265da5a 100644
--- a/examples/src/main/scala/tests/chakra/DataDisplay.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
@@ -26,7 +26,7 @@ object DataDisplay:
commonBox(text = "Tables"),
TableContainer().withChildren(
Table(variant = "striped", colorScheme = Some("teal"), size = "lg").withChildren(
- TableCaption(text = "Imperial to metric conversion factors"),
+ TableCaption(text = "Imperial to metric conversion factors (table-caption-0001)"),
Thead().withChildren(
headAndFoot
),
@@ -45,6 +45,11 @@ object DataDisplay:
Td(text = "yards"),
Td(text = "metres (m)"),
Td(text = "0.91444", isNumeric = true)
+ ),
+ Tr().withChildren(
+ Td(text = "td0001"),
+ Td(text = "td0002"),
+ Td(text = "td0003", isNumeric = true)
)
),
Tfoot().withChildren(
diff --git a/examples/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Editables.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Editables.scala
diff --git a/examples/src/main/scala/tests/chakra/Etc.scala b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
similarity index 57%
rename from examples/src/main/scala/tests/chakra/Etc.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
index f2470cb2..42b930c1 100644
--- a/examples/src/main/scala/tests/chakra/Etc.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
@@ -10,11 +10,11 @@ object Etc:
Seq(
commonBox(text = "Center"),
Center(text = "Center demo, not styled"),
- Center(text = "Center demo", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
+ Center(text = "Center demo center-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
commonBox(text = "Circle"),
Circle(text = "Circle demo, not styled"),
- Circle(text = "Circle demo", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
+ Circle(text = "Circle demo circle-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px")),
commonBox(text = "Square"),
Square(text = "Square demo, not styled"),
- Square(text = "Square demo", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px"))
+ Square(text = "Square demo square-demo-0001", bg = Some("tomato"), color = Some("white"), w = Some("100px"), h = Some("100px"))
)
diff --git a/examples/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Forms.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Forms.scala
diff --git a/examples/src/main/scala/tests/chakra/Grids.scala b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Grids.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Grids.scala
diff --git a/examples/src/main/scala/tests/chakra/MediaAndIcons.scala b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/MediaAndIcons.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala
diff --git a/examples/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
similarity index 84%
rename from examples/src/main/scala/tests/chakra/Overlay.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
index 13358751..75e672cb 100644
--- a/examples/src/main/scala/tests/chakra/Overlay.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
@@ -9,14 +9,14 @@ object Overlay:
def components(using session: ConnectedSession): Seq[UiElement] =
val box1 = Box(text = "Clicks will be reported here.")
Seq(
- commonBox(text = "Menus"),
+ commonBox(text = "Menus box0001"),
HStack().withChildren(
Menu().withChildren(
- MenuButton(text = "Actions", size = Some("sm"), colorScheme = Some("teal")).withChildren(
+ MenuButton(text = "Actions menu0001", size = Some("sm"), colorScheme = Some("teal")).withChildren(
ChevronDownIcon()
),
MenuList().withChildren(
- MenuItem(text = "Download")
+ MenuItem(text = "Download menu-download")
.onClick: () =>
box1.text = "'Download' clicked"
session.render()
diff --git a/examples/src/main/scala/tests/chakra/Stacks.scala b/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala
similarity index 100%
rename from examples/src/main/scala/tests/chakra/Stacks.scala
rename to end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Typography.scala b/end-to-end-tests/src/main/scala/tests/chakra/Typography.scala
new file mode 100644
index 00000000..d645fa75
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Typography.scala
@@ -0,0 +1,10 @@
+package tests.chakra
+
+import org.terminal21.client.components.UiElement
+import org.terminal21.client.components.chakra.*
+
+object Typography:
+ def components: Seq[UiElement] =
+ Seq(
+ Text(text = "typography-text-0001", color = Some("tomato"))
+ )
diff --git a/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveBarChart.scala b/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveBarChart.scala
new file mode 100644
index 00000000..787a9c88
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveBarChart.scala
@@ -0,0 +1,63 @@
+package tests.nivo
+
+import org.terminal21.client.components.nivo.*
+import tests.chakra.Common.commonBox
+
+import scala.util.Random
+
+object ResponsiveBarChart:
+ def apply() = Seq(
+ commonBox("ResponsiveBar"),
+ ResponsiveBar(
+ data = Seq(
+ dataFor("AD"),
+ dataFor("AE"),
+ dataFor("GB"),
+ dataFor("GR"),
+ dataFor("IT"),
+ dataFor("FR"),
+ dataFor("GE"),
+ dataFor("US")
+ ),
+ keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"),
+ indexBy = "country",
+ padding = 0.3,
+ defs = Seq(
+ Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)),
+ Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10))
+ ),
+ fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))),
+ axisLeft = Some(Axis(legend = "food", legendOffset = -40)),
+ axisBottom = Some(Axis(legend = "country", legendOffset = 32)),
+ valueScale = Scale(`type` = "linear"),
+ indexScale = Scale(`type` = "band", round = Some(true)),
+ legends = Seq(
+ Legend(
+ dataFrom = "keys",
+ translateX = 120,
+ itemsSpacing = 2,
+ itemWidth = 100,
+ itemHeight = 20,
+ symbolSize = 20,
+ symbolShape = "square"
+ )
+ )
+ )
+ )
+
+ def dataFor(country: String) =
+ Seq(
+ BarDatum("country", country),
+ BarDatum("hot dog", rnd),
+ BarDatum("hot dogColor", "hsl(202, 70%, 50%)"),
+ BarDatum("burger", rnd),
+ BarDatum("burgerColor", "hsl(106, 70%, 50%)"),
+ BarDatum("sandwich", rnd),
+ BarDatum("sandwichColor", "hsl(115, 70%, 50%)"),
+ BarDatum("kebab", rnd),
+ BarDatum("kebabColor", "hsl(113, 70%, 50%)"),
+ BarDatum("fries", rnd),
+ BarDatum("friesColor", "hsl(209, 70%, 50%)"),
+ BarDatum("donut", rnd),
+ BarDatum("donutColor", "hsl(47, 70%, 50%)")
+ )
diff --git a/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveLineChart.scala b/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveLineChart.scala
new file mode 100644
index 00000000..2646b092
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/nivo/ResponsiveLineChart.scala
@@ -0,0 +1,41 @@
+package tests.nivo
+
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.nivo.*
+import tests.chakra.Common
+import tests.chakra.Common.commonBox
+
+import scala.collection.immutable.Seq
+
+object ResponsiveLineChart:
+ def apply() = Seq(
+ commonBox("ResponsiveLine"),
+ ResponsiveLine(
+ data = Seq(
+ dataFor("Japan"),
+ dataFor("France"),
+ dataFor("Greece"),
+ dataFor("UK"),
+ dataFor("Germany")
+ ),
+ yScale = Scale(stacked = Some(true)),
+ axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "count", legendOffset = -40)),
+ legends = Seq(
+ Legend()
+ )
+ )
+ )
+
+ def dataFor(country: String) =
+ Serie(
+ country,
+ data = Seq(
+ Datum("plane", rnd),
+ Datum("helicopter", rnd),
+ Datum("boat", rnd),
+ Datum("car", rnd),
+ Datum("submarine", rnd)
+ )
+ )
diff --git a/end-to-end-tests/src/main/scala/tests/nivo/common.scala b/end-to-end-tests/src/main/scala/tests/nivo/common.scala
new file mode 100644
index 00000000..5a5c849c
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/nivo/common.scala
@@ -0,0 +1,5 @@
+package tests.nivo
+
+import scala.util.Random
+
+def rnd = Random.nextInt(500) + 50
diff --git a/example-scripts/bin/start-server b/example-scripts/bin/start-server
deleted file mode 100755
index b0dd2898..00000000
--- a/example-scripts/bin/start-server
+++ /dev/null
@@ -1,2 +0,0 @@
-#! /bin/sh
-scala-cli server.sc
diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc
index 558679bc..85330975 100755
--- a/example-scripts/csv-editor.sc
+++ b/example-scripts/csv-editor.sc
@@ -4,8 +4,6 @@
// A quick and dirty casv file editor for small csv files.
// ------------------------------------------------------------------------------
-//> using dep commons-io:commons-io:2.15.1
-
// always import these
import org.terminal21.client.*
// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc
index 0408d078..2ecd5a2d 100755
--- a/example-scripts/csv-viewer.sc
+++ b/example-scripts/csv-viewer.sc
@@ -4,8 +4,6 @@
// A csv file viewer
// ------------------------------------------------------------------------------
-//> using dep commons-io:commons-io:2.15.1
-
// always import these
import org.terminal21.client.*
// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc
new file mode 100755
index 00000000..902ff2e2
--- /dev/null
+++ b/example-scripts/mathjax.sc
@@ -0,0 +1,26 @@
+#!/usr/bin/env -S scala-cli project.scala
+
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.mathjax.*
+
+Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ): session =>
+ given ConnectedSession = session
+ Seq(
+ MathJax(
+ expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$"""
+ ),
+ MathJax(
+ expression = """
+ |when \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
+ |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam.
+ |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus.
+ |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus.
+ |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum.
+ |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi.
+ |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes,
+ |nascetur ridiculus mus.
+ |""".stripMargin
+ )
+ ).render()
+ session.waitTillUserClosesSession()
diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc
new file mode 100755
index 00000000..47ee7837
--- /dev/null
+++ b/example-scripts/nivo-bar-chart.sc
@@ -0,0 +1,84 @@
+#!/usr/bin/env -S scala-cli project.scala
+
+import org.terminal21.client.*
+import org.terminal21.client.fiberExecutor
+import org.terminal21.client.components.*
+import org.terminal21.client.components.nivo.*
+
+import scala.util.Random
+import NivoBarChart.*
+
+Sessions.withNewSession("nivo-bar-chart", "Nivo Bar Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session =>
+ given ConnectedSession = session
+
+ val chart = ResponsiveBar(
+ data = createRandomData,
+ keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"),
+ indexBy = "country",
+ padding = 0.3,
+ defs = Seq(
+ Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)),
+ Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10))
+ ),
+ fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))),
+ axisLeft = Some(Axis(legend = "food", legendOffset = -40)),
+ axisBottom = Some(Axis(legend = "country", legendOffset = 32)),
+ valueScale = Scale(`type` = "linear"),
+ indexScale = Scale(`type` = "band", round = Some(true)),
+ legends = Seq(
+ Legend(
+ dataFrom = "keys",
+ translateX = 120,
+ itemsSpacing = 2,
+ itemWidth = 100,
+ itemHeight = 20,
+ symbolSize = 20,
+ symbolShape = "square"
+ )
+ )
+ )
+
+ Seq(
+ Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)),
+ chart
+ ).render()
+
+ fiberExecutor.submit:
+ while !session.isClosed do
+ Thread.sleep(2000)
+ chart.data = createRandomData
+ session.render()
+
+ session.waitTillUserClosesSession()
+
+object NivoBarChart:
+ def createRandomData: Seq[Seq[BarDatum]] =
+ Seq(
+ dataFor("AD"),
+ dataFor("AE"),
+ dataFor("GB"),
+ dataFor("GR"),
+ dataFor("IT"),
+ dataFor("FR"),
+ dataFor("GE"),
+ dataFor("US")
+ )
+
+ def dataFor(country: String) =
+ Seq(
+ BarDatum("country", country),
+ BarDatum("hot dog", rnd),
+ BarDatum("hot dogColor", "hsl(202, 70%, 50%)"),
+ BarDatum("burger", rnd),
+ BarDatum("burgerColor", "hsl(106, 70%, 50%)"),
+ BarDatum("sandwich", rnd),
+ BarDatum("sandwichColor", "hsl(115, 70%, 50%)"),
+ BarDatum("kebab", rnd),
+ BarDatum("kebabColor", "hsl(113, 70%, 50%)"),
+ BarDatum("fries", rnd),
+ BarDatum("friesColor", "hsl(209, 70%, 50%)"),
+ BarDatum("donut", rnd),
+ BarDatum("donutColor", "hsl(47, 70%, 50%)")
+ )
+
+ def rnd = Random.nextInt(500) + 50
diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc
new file mode 100755
index 00000000..664f5698
--- /dev/null
+++ b/example-scripts/nivo-line-chart.sc
@@ -0,0 +1,56 @@
+#!/usr/bin/env -S scala-cli project.scala
+
+import org.terminal21.client.*
+import org.terminal21.client.fiberExecutor
+import org.terminal21.client.components.*
+import org.terminal21.client.components.nivo.*
+
+import scala.util.Random
+import NivoLineChart.*
+
+Sessions.withNewSession("nivo-line-chart", "Nivo Line Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session =>
+ given ConnectedSession = session
+
+ val chart = ResponsiveLine(
+ data = createRandomData,
+ yScale = Scale(stacked = Some(true)),
+ axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "count", legendOffset = -40)),
+ legends = Seq(Legend())
+ )
+
+ Seq(
+ Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)),
+ chart
+ ).render()
+
+ fiberExecutor.submit:
+ while !session.isClosed do
+ Thread.sleep(2000)
+ chart.data = createRandomData
+ session.render()
+
+ session.waitTillUserClosesSession()
+
+object NivoLineChart:
+ def createRandomData: Seq[Serie] =
+ Seq(
+ dataFor("Japan"),
+ dataFor("France"),
+ dataFor("Greece"),
+ dataFor("UK"),
+ dataFor("Germany")
+ )
+ def dataFor(country: String): Serie =
+ Serie(
+ country,
+ data = Seq(
+ Datum("plane", rnd), // rnd = random int, see below
+ Datum("helicopter", rnd),
+ Datum("boat", rnd),
+ Datum("car", rnd),
+ Datum("submarine", rnd)
+ )
+ )
+
+ def rnd = Random.nextInt(500) + 50
diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc
index cf5dceba..cca81ef6 100755
--- a/example-scripts/postit.sc
+++ b/example-scripts/postit.sc
@@ -25,7 +25,7 @@ Sessions.withNewSession("postit", "Post-It"): session =>
src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png",
boxSize = Some("32px")
),
- Box(text = editor.value)
+ Box(text = editor.value)
)
)
// always render after adding/modifying something
diff --git a/example-scripts/project.scala b/example-scripts/project.scala
index d2324063..dec52579 100644
--- a/example-scripts/project.scala
+++ b/example-scripts/project.scala
@@ -1,4 +1,7 @@
//> using jvm "21"
//> using scala 3
-//> using dep io.github.kostaskougios::terminal21-ui-std:0.1
+//> using dep io.github.kostaskougios::terminal21-ui-std:0.11
+//> using dep io.github.kostaskougios::terminal21-nivo:0.11
+//> using dep io.github.kostaskougios::terminal21-mathjax:0.11
+//> using dep commons-io:commons-io:2.15.1
diff --git a/example-scripts/server.sc b/example-scripts/server.sc
index 40f22515..b93acb6e 100755
--- a/example-scripts/server.sc
+++ b/example-scripts/server.sc
@@ -2,8 +2,8 @@
//> using jvm "21"
//> using scala 3
-//> using dep io.github.kostaskougios::terminal21-server:0.1
+//> using dep io.github.kostaskougios::terminal21-server:0.11
import org.terminal21.server.Terminal21Server
-Terminal21Server.start()
\ No newline at end of file
+Terminal21Server.start()
diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc
index db1177e4..dc894124 100755
--- a/example-scripts/textedit.sc
+++ b/example-scripts/textedit.sc
@@ -4,8 +4,6 @@
// A text file editor for small files.
// ------------------------------------------------------------------------------
-//> using dep commons-io:commons-io:2.15.1
-
import org.apache.commons.io.FileUtils
import java.io.File
diff --git a/example-spark/etc/logback.xml b/example-spark/etc/logback.xml
new file mode 100644
index 00000000..f037a072
--- /dev/null
+++ b/example-spark/etc/logback.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example-spark/model/Person.scala b/example-spark/model/Person.scala
new file mode 100644
index 00000000..d13eafba
--- /dev/null
+++ b/example-spark/model/Person.scala
@@ -0,0 +1,2 @@
+case class Person(id: Int, name: String, age: Int)
+
diff --git a/example-spark/project.scala b/example-spark/project.scala
new file mode 100644
index 00000000..f921305a
--- /dev/null
+++ b/example-spark/project.scala
@@ -0,0 +1,18 @@
+//> using jvm "21"
+//> using scala 3
+
+// these java params are needed for spark to work with jdk 21
+//> using javaOpt --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED
+
+// configure logback
+//> using javaOpt -Dlogback.configurationFile=file:etc/logback.xml
+
+// terminal21 dependencies
+//> using dep io.github.kostaskougios::terminal21-ui-std:0.11
+//> using dep io.github.kostaskougios::terminal21-spark:0.11
+//> using dep io.github.kostaskougios::terminal21-nivo:0.11
+//> using dep io.github.kostaskougios::terminal21-mathjax:0.11
+
+//> using dep ch.qos.logback:logback-classic:1.4.14
+
+//> using file model
diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc
new file mode 100755
index 00000000..c0cce5e2
--- /dev/null
+++ b/example-spark/spark-notebook.sc
@@ -0,0 +1,94 @@
+#!/usr/bin/env -S scala-cli --restart project.scala
+
+/**
+ * note we use the --restart param for scala-cli. This means every time we change this file, scala-cli will terminate
+ * and rerun it with the changes. This way we get the notebook feel when we use spark scripts.
+ *
+ * terminal21 spark lib caches datasets by storing them into disk. This way complex queries won't have to be re-evaluated
+ * on each restart of the script. We can force re-evaluation by clicking the "Recalculate" buttons in the UI.
+ */
+
+// We need these imports
+import org.apache.spark.sql.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.nivo.*
+import org.terminal21.client.{*, given}
+import org.terminal21.sparklib.*
+
+import java.util.concurrent.atomic.AtomicInteger
+import scala.util.Random
+import SparkNotebook.*
+import org.terminal21.client.components.mathjax.{MathJax, MathJaxLib}
+
+SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(/* configure your spark session here */), "spark-notebook", "Spark Notebook", NivoLib, MathJaxLib): (spark, session) =>
+ given ConnectedSession = session
+ given SparkSession = spark
+ import scala3encoders.given
+ import spark.implicits.*
+
+ // lets get a Dataset, the data are random so that when we click refresh we can see the data actually
+ // been refreshed.
+ val peopleDS = createPeople
+
+ // We will display the data in a table
+ val peopleTable = QuickTable().headers("Id", "Name", "Age").caption("People")
+
+ val peopleTableCalc = peopleDS.sort($"id").visualize("People sample", peopleTable): data =>
+ peopleTable.rows(data.take(5).map(p => Seq(p.id, p.name, p.age)))
+
+ /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long to
+ * calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is
+ * re-evaluated. If the Dataset schema changes, please click "Recalculate" or manually delete this folder.
+ *
+ * The key for the cache is "People sample"
+ */
+ println(s"Cache path: ${peopleTableCalc.cachePath}")
+
+ val oldestPeopleChart = ResponsiveLine(
+ axisBottom = Some(Axis(legend = "Person", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "Age", legendOffset = -40)),
+ legends = Seq(Legend())
+ )
+
+ val oldestPeopleChartCalc = peopleDS
+ .orderBy($"age".desc)
+ .visualize("Oldest people", oldestPeopleChart): data =>
+ oldestPeopleChart.data = Seq(
+ Serie(
+ "Person",
+ data = data.take(5).map(person => Datum(person.name, person.age))
+ )
+ )
+
+ Seq(
+ // just make it look a bit more like a proper notebook by adding some fake maths
+ MathJax(
+ expression = """
+ |The following is total nonsense but it simulates some explanation that would normally be here if we had
+ |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
+ |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam.
+ |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus.
+ |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus.
+ |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum.
+ |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi.
+ |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes,
+ |nascetur ridiculus mus.
+ |""".stripMargin
+ ),
+ peopleTableCalc,
+ oldestPeopleChartCalc
+ ).render()
+
+ session.waitTillUserClosesSession()
+
+object SparkNotebook:
+ private val names = Array("Andy", "Kostas", "Alex", "Andreas", "George", "Jack")
+ private val surnames = Array("Papadopoulos", "Rex", "Dylan", "Johnson", "Regan")
+ def randomName: String = names(Random.nextInt(names.length)) + " " + surnames(Random.nextInt(surnames.length))
+ def randomPerson(id: Int): Person = Person(id, randomName + s" ($id)", Random.nextInt(100))
+
+ def createPeople(using spark: SparkSession): Dataset[Person] =
+ import spark.implicits.*
+ import scala3encoders.given
+ (1 to 100).toDS.map(randomPerson)
diff --git a/examples/src/main/scala/examples/HelloWorld.scala b/examples/src/main/scala/examples/HelloWorld.scala
deleted file mode 100644
index 2a5530cf..00000000
--- a/examples/src/main/scala/examples/HelloWorld.scala
+++ /dev/null
@@ -1,38 +0,0 @@
-package examples
-
-import org.terminal21.client.components.*
-import org.terminal21.client.components.chakra.*
-import org.terminal21.client.{ConnectedSession, Sessions}
-
-@main def helloWorld(): Unit =
- Sessions.withNewSession("hello-world", "Hello World"): session =>
- given ConnectedSession = session
- println(session.session.id)
-
- val h1 = Header1(key = "header", text = "Welcome to the Hello World Program!")
- val b1 = Box(text = "First box", bg = "green", p = 4, color = "black")
- b1.withChildren(
- Button(text = "Click me!").onClick: () =>
- b1.text = "Clicked!"
- session.render()
- )
- val p1 = Paragraph(
- key = "status",
- text = s"Hello there mr X",
- children = Seq(
- b1,
- Box(text = "Second box", bg = "tomato", p = 4, color = "black")
- )
- )
- val grid = SimpleGrid(spacing = Some("8px"), columns = 4)
- session.add(h1, p1, grid)
- session.render()
-
- for i <- 1 to 25 do
- Thread.sleep(1000)
- p1.text = s"i = $i"
- grid.addChildren(Box(text = s"counting i = $i", bg = "green"))
- session.render()
-
- h1.text = "Done!"
- session.render()
diff --git a/examples/src/main/scala/tests/ChakraComponents.scala b/examples/src/main/scala/tests/ChakraComponents.scala
deleted file mode 100644
index 3b03ccb8..00000000
--- a/examples/src/main/scala/tests/ChakraComponents.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package tests
-
-import org.terminal21.client.*
-import org.terminal21.client.components.{Paragraph, render}
-import tests.chakra.*
-
-import java.util.concurrent.{CountDownLatch, TimeUnit}
-
-@main def chakraComponents(): Unit =
- Sessions.withNewSession("chakra-components", "Chakra Components"): session =>
- given ConnectedSession = session
-
- val latch = new CountDownLatch(1)
-
- (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components(
- latch
- ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components)
- .render()
-
- println("Waiting for button to be pressed for 1 hour")
- latch.await(1, TimeUnit.HOURS)
- session.clear()
- Paragraph(text = "Terminated").render()
diff --git a/img.png b/img.png
deleted file mode 100644
index ba3ef127..00000000
Binary files a/img.png and /dev/null differ
diff --git a/terminal21-client-common/src/main/scala/org/terminal21/client/ui/UiLib.scala b/terminal21-client-common/src/main/scala/org/terminal21/client/ui/UiLib.scala
deleted file mode 100644
index e9cfe8cd..00000000
--- a/terminal21-client-common/src/main/scala/org/terminal21/client/ui/UiLib.scala
+++ /dev/null
@@ -1,3 +0,0 @@
-package org.terminal21.client.ui
-
-trait UiLib
diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala
new file mode 100644
index 00000000..10fad50f
--- /dev/null
+++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala
@@ -0,0 +1,17 @@
+package org.terminal21.client.components.mathjax
+
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiElement}
+
+sealed trait MathJaxElement extends UiElement
+
+/** see https://asciimath.org/ and https://github.com/fast-reflexes/better-react-mathjax
+ */
+case class MathJax(
+ key: String = Keys.nextKey,
+ /** expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)"""
+ */
+ @volatile var expression: String = """fill in the expression as per https://asciimath.org/""",
+ @volatile var style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib
+) extends MathJaxElement
+ with HasStyle
diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala
new file mode 100644
index 00000000..5946f253
--- /dev/null
+++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala
@@ -0,0 +1,11 @@
+package org.terminal21.client.components.mathjax
+
+import io.circe.generic.auto.*
+import io.circe.syntax.*
+import io.circe.*
+import org.terminal21.client.components.{ComponentLib, UiElement}
+
+object MathJaxLib extends ComponentLib:
+ import org.terminal21.client.components.StdElementEncoding.given
+ override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
+ case n: MathJaxElement => n.asJson.mapObject(o => o.add("type", "MathJax".asJson))
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Axis.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Axis.scala
new file mode 100644
index 00000000..4bad6fcc
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Axis.scala
@@ -0,0 +1,10 @@
+package org.terminal21.client.components.nivo
+
+case class Axis(
+ tickSize: Int = 5,
+ tickPadding: Int = 5,
+ tickRotation: Int = 0,
+ legend: String = "CHANGEME axis.legend",
+ legendOffset: Int = 0,
+ legendPosition: String = "middle"
+)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/BarDatum.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/BarDatum.scala
new file mode 100644
index 00000000..3452a5ed
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/BarDatum.scala
@@ -0,0 +1,23 @@
+package org.terminal21.client.components.nivo
+
+import io.circe.{Encoder, Json}
+
+case class BarDatum(
+ name: String,
+ value: Any // String, Int, Float etc, see the encoder below for supported types
+)
+
+object BarDatum:
+ given Encoder[Seq[BarDatum]] = s =>
+ val vs = s.map: bd =>
+ (
+ bd.name,
+ bd.value match
+ case s: String => Json.fromString(s)
+ case i: Int => Json.fromInt(i)
+ case f: Float => Json.fromFloat(f).get
+ case d: Double => Json.fromDouble(d).get
+ case b: Boolean => Json.fromBoolean(b)
+ case _ => throw new IllegalArgumentException(s"type $bd not supported, either use one of the supported ones or open a bug request")
+ )
+ Json.obj(vs: _*)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Defs.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Defs.scala
new file mode 100644
index 00000000..5e7a3c8a
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Defs.scala
@@ -0,0 +1,14 @@
+package org.terminal21.client.components.nivo
+
+case class Defs(
+ id: String = "dots",
+ `type`: String = "patternDots",
+ background: String = "inherit",
+ color: String = "#38bcb2",
+ size: Option[Int] = None,
+ padding: Option[Float] = None,
+ stagger: Option[Boolean] = None,
+ rotation: Option[Int] = None,
+ lineWidth: Option[Int] = None,
+ spacing: Option[Int] = None
+)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Effect.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Effect.scala
new file mode 100644
index 00000000..7fb5c73a
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Effect.scala
@@ -0,0 +1,9 @@
+package org.terminal21.client.components.nivo
+
+case class Effect(
+ on: String = "hover",
+ style: Map[String, Any] = Map(
+ "itemBackground" -> "rgba(0, 0, 0, .03)",
+ "itemOpacity" -> 1
+ )
+)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Fill.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Fill.scala
new file mode 100644
index 00000000..b7bd47f9
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Fill.scala
@@ -0,0 +1,8 @@
+package org.terminal21.client.components.nivo
+
+case class Fill(
+ id: String,
+ `match`: Match
+)
+
+case class Match(id: String)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Legend.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Legend.scala
new file mode 100644
index 00000000..769c658f
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Legend.scala
@@ -0,0 +1,19 @@
+package org.terminal21.client.components.nivo
+
+case class Legend(
+ dataFrom: String = "keys",
+ anchor: String = "bottom-right",
+ direction: String = "column",
+ justify: Boolean = false,
+ translateX: Int = 100,
+ translateY: Int = 0,
+ itemsSpacing: Int = 0,
+ itemDirection: String = "left-to-right",
+ itemWidth: Int = 80,
+ itemHeight: Int = 20,
+ itemOpacity: Float = 0.75,
+ symbolSize: Int = 12,
+ symbolShape: String = "circle",
+ symbolBorderColor: String = "rgba(0, 0, 0, .5)",
+ effects: Seq[Effect] = Seq(Effect())
+)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Margin.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Margin.scala
new file mode 100644
index 00000000..d30a7519
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Margin.scala
@@ -0,0 +1,3 @@
+package org.terminal21.client.components.nivo
+
+case class Margin(top: Int = 50, right: Int = 50, bottom: Int = 50, left: Int = 50)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala
new file mode 100644
index 00000000..7200fc36
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala
@@ -0,0 +1,58 @@
+package org.terminal21.client.components.nivo
+
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiElement}
+
+sealed trait NivoElement extends UiElement
+
+/** https://nivo.rocks/line/
+ */
+case class ResponsiveLine(
+ key: String = Keys.nextKey,
+ // to give width and height, we wrap the component in a wrapper element. Height must be provided
+ // for nivo components to be visible
+ @volatile var style: Map[String, Any] = Map("height" -> "400px"),
+ @volatile var data: Seq[Serie] = Nil,
+ @volatile var margin: Margin = Margin(right = 110),
+ @volatile var xScale: Scale = Scale.Point,
+ @volatile var yScale: Scale = Scale(),
+ @volatile var yFormat: String = " >-.2f",
+ @volatile var axisTop: Option[Axis] = None,
+ @volatile var axisRight: Option[Axis] = None,
+ @volatile var axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)),
+ @volatile var axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)),
+ @volatile var pointSize: Int = 10,
+ @volatile var pointColor: Map[String, String] = Map("theme" -> "background"),
+ @volatile var pointBorderWidth: Int = 2,
+ @volatile var pointBorderColor: Map[String, String] = Map("from" -> "serieColor"),
+ @volatile var pointLabelYOffset: Int = -12,
+ @volatile var useMesh: Boolean = true,
+ @volatile var legends: Seq[Legend] = Nil
+) extends NivoElement
+ with HasStyle
+
+/** https://nivo.rocks/bar/
+ */
+case class ResponsiveBar(
+ key: String = Keys.nextKey,
+ // to give width and height, we wrap the component in a wrapper element. Height must be provided
+ // for nivo components to be visible
+ @volatile var style: Map[String, Any] = Map("height" -> "400px"),
+ @volatile var data: Seq[Seq[BarDatum]] = Nil,
+ @volatile var keys: Seq[String] = Nil,
+ @volatile var indexBy: String = "",
+ @volatile var margin: Margin = Margin(right = 110),
+ @volatile var padding: Float = 0,
+ @volatile var valueScale: Scale = Scale(),
+ @volatile var indexScale: Scale = Scale(),
+ @volatile var colors: Map[String, String] = Map("scheme" -> "nivo"),
+ @volatile var defs: Seq[Defs] = Nil,
+ @volatile var fill: Seq[Fill] = Nil,
+ @volatile var axisTop: Option[Axis] = None,
+ @volatile var axisRight: Option[Axis] = None,
+ @volatile var axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)),
+ @volatile var axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)),
+ @volatile var legends: Seq[Legend] = Nil,
+ @volatile var ariaLabel: String = "Chart Label"
+) extends NivoElement
+ with HasStyle
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala
new file mode 100644
index 00000000..d4030b54
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala
@@ -0,0 +1,12 @@
+package org.terminal21.client.components.nivo
+
+import io.circe.{Encoder, Json}
+import org.terminal21.client.components.{ComponentLib, UiElement}
+import io.circe.*
+import io.circe.generic.auto.*
+import io.circe.syntax.*
+
+object NivoLib extends ComponentLib:
+ import org.terminal21.client.components.StdElementEncoding.given
+ override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
+ case n: NivoElement => n.asJson.mapObject(o => o.add("type", "Nivo".asJson))
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Scale.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Scale.scala
new file mode 100644
index 00000000..89af9a26
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Scale.scala
@@ -0,0 +1,13 @@
+package org.terminal21.client.components.nivo
+
+case class Scale(
+ `type`: String = "linear",
+ min: Option[String] = Some("auto"),
+ max: Option[String] = Some("auto"),
+ stacked: Option[Boolean] = Some(false),
+ reverse: Option[Boolean] = Some(false),
+ round: Option[Boolean] = None
+)
+
+object Scale:
+ val Point = Scale(`type` = "point", None, None, None, None)
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Serie.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Serie.scala
new file mode 100644
index 00000000..8b94cea0
--- /dev/null
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/Serie.scala
@@ -0,0 +1,24 @@
+package org.terminal21.client.components.nivo
+
+import io.circe.{Encoder, Json}
+
+case class Serie(
+ id: String,
+ color: String = "hsl(88, 70%, 50%)",
+ data: Seq[Datum] = Nil
+)
+
+case class Datum(
+ x: String | Int | Float,
+ y: String | Int | Float
+)
+
+object Datum:
+ private def toJson(name: String, x: String | Int | Float): (String, Json) = x match
+ case s: String => (name, Json.fromString(s))
+ case i: Int => (name, Json.fromInt(i))
+ case f: Float => (name, Json.fromFloat(f).get)
+
+ given Encoder[Datum] =
+ case Datum(x, y) =>
+ Json.obj(toJson("x", x), toJson("y", y))
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala b/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala
index ef907e8d..70e8be52 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala
+++ b/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala
@@ -29,7 +29,7 @@ object Terminal21Server:
if !server.isRunning then throw new IllegalStateException("Server failed to start")
try
- logger.info(s"Terminal 21 Server started and listening on http://localhost:$portV")
+ logger.info(s"Terminal 21 Server started. Please open http://localhost:$portV/ui for the user interface")
val hostname = InetAddress.getLocalHost.getHostName
logger.info(s"""
|Files under ~/.terminal21/web will be served under /web
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala
new file mode 100644
index 00000000..55e63694
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala
@@ -0,0 +1,26 @@
+package org.terminal21.sparklib
+
+import functions.fibers.FiberExecutor
+import org.apache.spark.sql.SparkSession
+import org.terminal21.client.ConnectedSession
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiElement}
+import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation}
+
+extension [OUT: ReadWriter](ds: OUT)
+ def visualize(name: String, dataUi: UiElement with HasStyle)(
+ toUi: OUT => Unit
+ )(using
+ session: ConnectedSession,
+ executor: FiberExecutor,
+ spark: SparkSession
+ ) =
+ val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi):
+ override protected def whenResultsReady(results: OUT): Unit =
+ try toUi(results)
+ catch case t: Throwable => t.printStackTrace()
+ super.whenResultsReady(results)
+ override def nonCachedCalculation: OUT = ds
+
+ ui.run()
+ ui
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/DataframeExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/DataframeExtensions.scala
new file mode 100644
index 00000000..0ad266e8
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/DataframeExtensions.scala
@@ -0,0 +1,7 @@
+package org.terminal21.sparklib
+
+import org.apache.spark.sql.{DataFrame, Row}
+
+extension (rows: Seq[Row])
+ def toUiTable: Seq[Seq[String]] = rows.map: row =>
+ row.toSeq.map(_.toString)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessionExt.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessionExt.scala
new file mode 100644
index 00000000..f07fd00b
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessionExt.scala
@@ -0,0 +1,16 @@
+package org.terminal21.sparklib
+
+import org.apache.spark.sql.{DataFrame, Dataset, Encoder, SparkSession}
+
+import scala.reflect.ClassTag
+
+class SparkSessionExt(spark: SparkSession):
+ import spark.implicits.*
+
+ def schemaOf[P: Encoder] = summon[Encoder[P]].schema
+
+ def toDF[P: ClassTag: Encoder](s: Seq[P], numSlices: Int = spark.sparkContext.defaultParallelism): DataFrame =
+ spark.sparkContext.parallelize(s, numSlices).toDF()
+
+ def toDS[P: ClassTag: Encoder](s: Seq[P], numSlices: Int = spark.sparkContext.defaultParallelism): Dataset[P] =
+ spark.sparkContext.parallelize(s, numSlices).toDS()
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala
new file mode 100644
index 00000000..f7306ac1
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala
@@ -0,0 +1,43 @@
+package org.terminal21.sparklib
+
+import org.apache.spark.sql.SparkSession
+import org.terminal21.client.components.ComponentLib
+import org.terminal21.client.{ConnectedSession, Sessions}
+
+import scala.util.Using
+
+object SparkSessions:
+ def newSparkSession(
+ appName: String = "spark-app",
+ master: String = "local[*]",
+ bindAddress: String = "localhost",
+ sparkUiEnabled: Boolean = false
+ ): SparkSession =
+ SparkSession
+ .builder()
+ .appName(appName)
+ .master(master)
+ .config("spark.driver.bindAddress", bindAddress)
+ .config("spark.ui.enabled", sparkUiEnabled)
+ .getOrCreate()
+
+ /** Will create a terminal21 session and use the provided spark session
+ * @param spark
+ * the spark session, will be closed before this call returns. Use #newSparkSession to quickly create one.
+ * @param id
+ * the id of the terminal21 session
+ * @param name
+ * the name of the terminal21 session
+ * @param f
+ * the code to run
+ * @tparam R
+ * if f returns some value, this will be returned by the method
+ * @return
+ * whatever f returns
+ */
+ def newTerminal21WithSparkSession[R](spark: SparkSession, id: String, name: String, componentLibs: ComponentLib*)(
+ f: (SparkSession, ConnectedSession) => R
+ ): R =
+ Sessions.withNewSession(id, name, componentLibs: _*): terminal21Session =>
+ Using.resource(spark): _ =>
+ f(spark, terminal21Session)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/ReadWriter.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/ReadWriter.scala
new file mode 100644
index 00000000..1aa716bd
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/ReadWriter.scala
@@ -0,0 +1,19 @@
+package org.terminal21.sparklib.calculations
+
+import org.apache.spark.sql.{DataFrame, Dataset, Encoder, SparkSession}
+
+import scala.annotation.implicitNotFound
+
+@implicitNotFound("Unable to find ReadWriter for type ${A}. Dataset of case classes and Dataframes are supported.")
+trait ReadWriter[A]:
+ def read(spark: SparkSession, file: String): A
+ def write(file: String, ds: A): Unit
+
+object ReadWriter:
+ given datasetReadWriter[A](using Encoder[A]): ReadWriter[Dataset[A]] = new ReadWriter[Dataset[A]]:
+ override def read(spark: SparkSession, file: String) = spark.read.parquet(file).as[A]
+ override def write(file: String, ds: Dataset[A]): Unit = ds.write.parquet(file)
+
+ given dataframeReadWriter: ReadWriter[DataFrame] = new ReadWriter[DataFrame]:
+ override def read(spark: SparkSession, file: String) = spark.read.parquet(file)
+ override def write(file: String, ds: DataFrame): Unit = ds.write.parquet(file)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala
new file mode 100644
index 00000000..10b20321
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala
@@ -0,0 +1,54 @@
+package org.terminal21.sparklib.calculations
+
+import functions.fibers.FiberExecutor
+import org.apache.commons.io.FileUtils
+import org.apache.spark.sql.SparkSession
+import org.terminal21.client.ConnectedSession
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement}
+import org.terminal21.sparklib.util.Environment
+
+import java.io.File
+
+/** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp
+ * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun
+ * the spark calculations in case the data changed.
+ *
+ * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without
+ * having to rerun the spark calculation.
+ *
+ * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below.
+ */
+trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent:
+ private val rw = implicitly[ReadWriter[OUT]]
+ private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations"
+ private val targetDir = s"$rootFolder/$name"
+
+ def isCached: Boolean = new File(targetDir).exists()
+ def cachePath: String = targetDir
+
+ private def cache[A](reader: => A, writer: => A): A =
+ if isCached then reader
+ else writer
+
+ override def invalidateCache(): Unit =
+ FileUtils.deleteDirectory(new File(targetDir))
+
+ private def calculateOnce(f: => OUT): OUT =
+ cache(
+ rw.read(spark, targetDir), {
+ val ds = f
+ rw.write(targetDir, ds)
+ ds
+ }
+ )
+
+ override protected def calculation(): OUT = calculateOnce(nonCachedCalculation)
+
+abstract class StdUiSparkCalculation[OUT: ReadWriter](
+ val key: String,
+ name: String,
+ dataUi: UiElement with HasStyle
+)(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession)
+ extends SparkCalculation[OUT](name)
+ with StdUiCalculation[OUT](name, dataUi)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/util/Environment.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/util/Environment.scala
new file mode 100644
index 00000000..ae00d0d0
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/util/Environment.scala
@@ -0,0 +1,8 @@
+package org.terminal21.sparklib.util
+
+import org.apache.commons.lang3.StringUtils
+
+object Environment:
+ val tmpDirectory =
+ val t = System.getProperty("java.io.tmpdir")
+ if (t.endsWith("/")) StringUtils.substringBeforeLast(t, "/") else t
diff --git a/terminal21-spark/src/test/resources/logback-test.xml b/terminal21-spark/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..f037a072
--- /dev/null
+++ b/terminal21-spark/src/test/resources/logback-test.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/AbstractSparkSuite.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/AbstractSparkSuite.scala
new file mode 100644
index 00000000..61a8dcd7
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/AbstractSparkSuite.scala
@@ -0,0 +1,11 @@
+package org.terminal21.sparklib
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers
+import org.terminal21.sparklib.util.Environment
+
+import java.util.UUID
+
+class AbstractSparkSuite extends AnyFunSuiteLike with Matchers:
+ protected def randomString: String = UUID.randomUUID().toString
+ protected def randomTmpFilename: String = s"${Environment.tmpDirectory}/AbstractSparkSuite-" + UUID.randomUUID().toString
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionExtTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionExtTest.scala
new file mode 100644
index 00000000..951daccd
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionExtTest.scala
@@ -0,0 +1,32 @@
+package org.terminal21.sparklib
+
+import org.terminal21.sparklib.testmodel.Person
+
+import scala.util.Using
+
+class SparkSessionExtTest extends AbstractSparkSuite:
+ val people = for (i <- 1 to 10) yield Person(i.toString, s"text for row $i")
+
+ test("schemaOf"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ val sp = new SparkSessionExt(spark)
+ import scala3encoders.given
+ import spark.implicits.*
+ val schema = sp.schemaOf[Person]
+ schema.toList.size should be(2)
+
+ test("toDF"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ val sp = new SparkSessionExt(spark)
+ import scala3encoders.given
+ import spark.implicits.*
+ val df = sp.toDF(people)
+ df.as[Person].collect() should be(people.toArray)
+
+ test("toDS"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ val sp = new SparkSessionExt(spark)
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = sp.toDS(people)
+ ds.collect() should be(people.toArray)
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionsTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionsTest.scala
new file mode 100644
index 00000000..096668da
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/SparkSessionsTest.scala
@@ -0,0 +1,74 @@
+package org.terminal21.sparklib
+
+import org.terminal21.sparklib.testmodel.Person
+
+import scala.util.Using
+
+class SparkSessionsTest extends AbstractSparkSuite:
+ val people = for (i <- 1 to 10) yield Person(i.toString, s"text for row $i")
+
+ test("creates/destroys session"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ ()
+
+ test("Can convert to Dataframe"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val df = spark.sparkContext.parallelize(people, 16).toDF()
+ df.as[Person].collect() should be(people.toArray)
+
+ test("Can convert to Dataset"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ ds.collect() should be(people.toArray)
+
+ test("Can write parquet"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ val f = randomTmpFilename
+ ds.write.parquet(f)
+ val rds = spark.read.parquet(f).as[Person]
+ rds.collect() should be(rds.collect())
+
+ test("Can write csv"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ val f = randomTmpFilename
+ ds.write.option("header", true).csv(f)
+ val rds = spark.read.option("header", true).csv(f).as[Person]
+ rds.collect() should be(rds.collect())
+
+ test("Can write json"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ val f = randomTmpFilename
+ ds.write.json(f)
+ val rds = spark.read.json(f).as[Person]
+ rds.collect() should be(rds.collect())
+
+ test("Can write orc"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ val f = randomTmpFilename
+ ds.write.orc(f)
+ val rds = spark.read.orc(f).as[Person]
+ rds.collect() should be(rds.collect())
+
+ test("Can mount as tmp table"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import scala3encoders.given
+ import spark.implicits.*
+ val ds = spark.sparkContext.parallelize(people, 16).toDS()
+ ds.createOrReplaceTempView("people")
+ spark.sql("select * from people").count() should be(10)
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala
new file mode 100644
index 00000000..13c7aadd
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala
@@ -0,0 +1,99 @@
+package org.terminal21.sparklib.calculations
+
+import org.apache.spark.sql.{Dataset, Encoder, SparkSession}
+import org.scalatest.concurrent.Eventually
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.scalatest.time.{Millis, Span}
+import org.terminal21.client.components.Keys
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given}
+import org.terminal21.sparklib.SparkSessions
+
+import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
+import scala.util.Using
+
+class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually:
+ given PatienceConfig = PatienceConfig(scaled(Span(3000, Millis)))
+
+ test("calculates the correct result"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val calc = new TestingCalculation
+ calc.run().get().collect().toList should be(List(2))
+
+ test("whenResultsNotReady"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val called = new AtomicBoolean(false)
+ val calc = new TestingCalculation:
+ override protected def whenResultsNotReady(): Unit =
+ called.set(true)
+ calc.run().get()
+ called.get() should be(true)
+
+ test("whenResultsReady"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val called = new AtomicBoolean(false)
+ val calc = new TestingCalculation:
+ override protected def whenResultsReady(results: Dataset[Int]): Unit =
+ results.collect().toList should be(List(2))
+ called.set(true)
+
+ calc.run()
+ eventually:
+ called.get() should be(true)
+
+ test("whenResultsReady called even when cached"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val called = new AtomicInteger(0)
+ val calc = new TestingCalculation:
+ override protected def whenResultsReady(results: Dataset[Int]): Unit =
+ results.collect().toList should be(List(2))
+ called.incrementAndGet()
+
+ calc.run().get()
+ calc.run()
+ eventually:
+ called.get() should be(2)
+
+ test("caches results"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val calc = new TestingCalculation
+ calc.run().get().collect().toList should be(List(2))
+ calc.run().get().collect().toList should be(List(2))
+ calc.calcCalledTimes.get() should be(1)
+
+ test("refresh button invalidates cache and runs calculations"):
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ import spark.implicits.*
+ given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ given SparkSession = spark
+ val calc = new TestingCalculation
+ calc.run().get().collect().toList should be(List(2))
+ session.click(calc.recalc)
+ eventually:
+ calc.calcCalledTimes.get() should be(2)
+
+class TestingCalculation(using session: ConnectedSession, spark: SparkSession, enc: Encoder[Int])
+ extends StdUiSparkCalculation[Dataset[Int]](Keys.nextKey, "testing-calc", Box()):
+ val calcCalledTimes = new AtomicInteger(0)
+ invalidateCache()
+
+ override def nonCachedCalculation: Dataset[Int] =
+ import spark.implicits.*
+ calcCalledTimes.incrementAndGet()
+ Seq(2).toDS
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala
new file mode 100644
index 00000000..1b391b16
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala
@@ -0,0 +1,80 @@
+package org.terminal21.sparklib.endtoend
+
+import org.apache.commons.lang3.StringUtils
+import org.apache.spark.sql.{Dataset, SparkSession}
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.nivo.*
+import org.terminal21.client.{*, given}
+import org.terminal21.sparklib.*
+import org.terminal21.sparklib.endtoend.model.CodeFile
+import org.terminal21.sparklib.endtoend.model.CodeFile.scanSourceFiles
+
+@main def sparkBasics(): Unit =
+ SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(), "spark-basics", "Spark Basics", NivoLib): (spark, session) =>
+ given ConnectedSession = session
+ given SparkSession = spark
+ import scala3encoders.given
+ import spark.implicits.*
+
+ val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp")
+
+ val sortedFilesTable = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords")
+ val codeFilesTable = QuickTable().headers(headers: _*).caption("Unsorted files")
+
+ val sortedSourceFilesDS = sortedSourceFiles(sourceFiles())
+ val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results =>
+ val tableRows = results.take(3).toList.map(_.toData)
+ sortedFilesTable.rows(tableRows)
+
+ val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results =>
+ val dt = results.take(3).toList
+ codeFilesTable.rows(dt.map(_.toData))
+
+ val sortedFilesTableDF = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF")
+ val sortedCalcAsDF = sourceFiles()
+ .sort($"createdDate".asc, $"numOfWords".asc)
+ .toDF()
+ .visualize("Sorted files DF", sortedFilesTableDF): results =>
+ val tableRows = results.take(4).toList
+ sortedFilesTableDF.rows(tableRows.toUiTable)
+
+ val chart = ResponsiveLine(
+ data = Seq(
+ Serie(
+ "Scala",
+ data = Seq(
+ Datum("plane", 262),
+ Datum("helicopter", 26),
+ Datum("boat", 43)
+ )
+ )
+ ),
+ axisBottom = Some(Axis(legend = "Class", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "Count", legendOffset = -40)),
+ legends = Seq(Legend())
+ )
+
+ val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results =>
+ val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList
+ chart.data = Seq(Serie("Scala", data = data))
+ session.render()
+
+ Seq(
+ codeFilesCalculation,
+ sortedCalc,
+ sortedCalcAsDF,
+ sourceFileChart
+ ).render()
+
+ session.waitTillUserClosesSession()
+
+def sourceFiles()(using spark: SparkSession) =
+ import scala3encoders.given
+ import spark.implicits.*
+ scanSourceFiles.toDS.map: cf =>
+ cf.copy(timestamp = System.currentTimeMillis())
+
+def sortedSourceFiles(sourceFiles: Dataset[CodeFile])(using spark: SparkSession) =
+ import spark.implicits.*
+ sourceFiles.sort($"createdDate".desc, $"numOfWords".desc)
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/model/CodeFile.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/model/CodeFile.scala
new file mode 100644
index 00000000..0113b1ef
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/model/CodeFile.scala
@@ -0,0 +1,27 @@
+package org.terminal21.sparklib.endtoend.model
+
+import org.apache.commons.io.FileUtils
+
+import java.io.File
+import java.nio.file.Files
+import java.time.{Instant, LocalDate, ZoneId}
+
+case class CodeFile(id: Int, name: String, path: String, numOfLines: Int, numOfWords: Int, createdDate: LocalDate, timestamp: Long):
+ def toColumnNames: Seq[String] = productElementNames.toList
+ def toData: Seq[String] = productIterator.map(_.toString).toList
+
+object CodeFile:
+ import scala.jdk.CollectionConverters.*
+ def scanSourceFiles: Seq[CodeFile] =
+ val availableFiles = FileUtils.listFiles(new File(".."), Array("scala"), true).asScala.filterNot(_.getPath.contains("/.scala-build/")).toList
+ availableFiles.zipWithIndex.map: (f, i) =>
+ val code = Files.readString(f.toPath)
+ CodeFile(
+ i,
+ f.getName,
+ f.getPath,
+ code.split("\n").length,
+ code.split(" ").length,
+ LocalDate.ofInstant(Instant.ofEpochMilli(f.lastModified()), ZoneId.systemDefault()),
+ System.currentTimeMillis()
+ )
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/testmodel/Person.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/testmodel/Person.scala
new file mode 100644
index 00000000..286580a5
--- /dev/null
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/testmodel/Person.scala
@@ -0,0 +1,3 @@
+package org.terminal21.sparklib.testmodel
+
+case class Person(id: String, name: String)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
index 4e32c05e..b5a73373 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
@@ -6,13 +6,15 @@ import io.circe.syntax.*
import org.slf4j.LoggerFactory
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.UiElement.{HasEventHandler, allDeep}
-import org.terminal21.client.components.UiElementEncoding.uiElementEncoder
+import org.terminal21.client.components.UiElementEncoding
import org.terminal21.model.*
import org.terminal21.ui.std.SessionsService
-import java.util.concurrent.CountDownLatch
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.{CountDownLatch, TimeUnit}
+import scala.annotation.tailrec
-class ConnectedSession(val session: Session, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit):
+class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit):
private val logger = LoggerFactory.getLogger(getClass)
private var elements = List.empty[UiElement]
@@ -39,12 +41,39 @@ class ConnectedSession(val session: Session, val serverUrl: String, sessionsServ
val handlers = eventHandlers.getOrElse(key, Nil)
eventHandlers += key -> (handlers :+ handler)
- private val exitLatch = new CountDownLatch(1)
+ private val exitLatch = new CountDownLatch(1)
+
+ /** Waits till user closes the session by clicking the session close [X] button.
+ */
def waitTillUserClosesSession(): Unit =
try exitLatch.await()
catch case _: Throwable => () // nop
- def fireEvent(event: CommandEvent): Unit =
+ private val leaveSessionOpen = new AtomicBoolean(false)
+
+ /** Doesn't close the session upon exiting. In the UI the session seems active but events are not working because the event handlers are not available.
+ */
+ def leaveSessionOpenAfterExiting(): Unit =
+ leaveSessionOpen.set(true)
+
+ def isLeaveSessionOpen: Boolean = leaveSessionOpen.get()
+
+ /** Waits till user closes the session or a custom condition becomes true
+ * @param condition
+ * if true then this returns otherwise it waits.
+ */
+ @tailrec final def waitTillUserClosesSessionOr(condition: => Boolean): Unit =
+ exitLatch.await(100, TimeUnit.MILLISECONDS)
+ if exitLatch.getCount == 0 || condition then () else waitTillUserClosesSessionOr(condition)
+
+ /** @return
+ * true if user closed the session via the close button
+ */
+ def isClosed: Boolean = exitLatch.getCount == 0
+
+ def click(e: UiElement): Unit = fireEvent(OnClick(e.key))
+
+ private[client] def fireEvent(event: CommandEvent): Unit =
event match
case SessionClosed(_) =>
exitLatch.countDown()
@@ -65,8 +94,11 @@ class ConnectedSession(val session: Session, val serverUrl: String, sessionsServ
val j = toJson
sessionsService.setSessionJsonState(session, j.toJson.noSpaces)
+ def allElements: Seq[UiElement] = synchronized(elements)
+
private def toJson: JsonObject =
- val elementsCopy = synchronized(elements)
+ import encoding.given
+ val elementsCopy = allElements
val json =
for e <- elementsCopy
yield e.asJson.deepDropNullValues
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala
new file mode 100644
index 00000000..74a3af9b
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala
@@ -0,0 +1,5 @@
+package org.terminal21.client
+
+import functions.fibers.FiberExecutor
+
+given fiberExecutor: FiberExecutor = FiberExecutor()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
index 00370517..5b29b5fd 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
@@ -4,13 +4,14 @@ import functions.fibers.FiberExecutor
import functions.helidon.transport.HelidonTransport
import io.helidon.webclient.api.WebClient
import io.helidon.webclient.websocket.WsClient
+import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding}
import org.terminal21.config.Config
import org.terminal21.ui.std.SessionsServiceCallerFactory
import java.util.concurrent.atomic.AtomicBoolean
object Sessions:
- def withNewSession[R](id: String, name: String)(f: ConnectedSession => R): R =
+ def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R =
val config = Config.Default
val serverUrl = s"http://${config.host}:${config.port}"
val client = WebClient.builder
@@ -23,13 +24,12 @@ object Sessions:
.baseUri(s"ws://${config.host}:${config.port}")
.build
- val currentThread = Thread.currentThread()
val isStopped = new AtomicBoolean(false)
def terminate(): Unit =
isStopped.set(true)
- currentThread.interrupt()
- val connectedSession = ConnectedSession(session, serverUrl, sessionsService, terminate)
+ val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs)
+ val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate)
FiberExecutor.withFiberExecutor: executor =>
val listener = new ClientEventsWsListener(wsClient, connectedSession, executor)
listener.start()
@@ -37,5 +37,5 @@ object Sessions:
try {
f(connectedSession)
} finally
- if !isStopped.get() then sessionsService.terminateSession(session)
+ if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session)
listener.close()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala
new file mode 100644
index 00000000..98a95d8d
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala
@@ -0,0 +1,11 @@
+package org.terminal21.client.components
+
+import functions.fibers.{Fiber, FiberExecutor}
+
+abstract class CachedCalculation[OUT](using executor: FiberExecutor) extends Calculation[OUT]:
+ def isCached: Boolean
+ def invalidateCache(): Unit
+ def nonCachedCalculation: OUT
+ override def reCalculate(): Fiber[OUT] =
+ invalidateCache()
+ super.reCalculate()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala
new file mode 100644
index 00000000..30c933e0
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala
@@ -0,0 +1,33 @@
+package org.terminal21.client.components
+
+import functions.fibers.{Fiber, FiberExecutor}
+
+import java.util.concurrent.CountDownLatch
+
+trait Calculation[OUT](using executor: FiberExecutor):
+ protected def calculation(): OUT
+ protected def whenResultsNotReady(): Unit = ()
+ protected def whenResultsReady(results: OUT): Unit = ()
+
+ def reCalculate(): Fiber[OUT] = run()
+
+ def onError(t: Throwable): Unit =
+ t.printStackTrace()
+
+ def run(): Fiber[OUT] =
+ val refreshInOrder = new CountDownLatch(1)
+ executor.submit:
+ try
+ executor.submit:
+ try whenResultsNotReady()
+ finally refreshInOrder.countDown()
+
+ val out = calculation()
+ refreshInOrder.await()
+ executor.submit:
+ whenResultsReady(out)
+ out
+ catch
+ case t: Throwable =>
+ onError(t)
+ throw t
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ComponentLib.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ComponentLib.scala
new file mode 100644
index 00000000..dd47bf23
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ComponentLib.scala
@@ -0,0 +1,6 @@
+package org.terminal21.client.components
+
+import io.circe.{Encoder, Json}
+
+trait ComponentLib:
+ def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json]
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
index 6712b29c..1c4d316b 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
@@ -1,20 +1,20 @@
package org.terminal21.client.components
-import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler}
+import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle}
import org.terminal21.client.{ConnectedSession, OnChangeEventHandler}
-sealed trait StdElement extends UiElement
+sealed trait StdElement extends UiElement with HasStyle
-case class Span(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, String] = Map.empty) extends StdElement
-case class NewLine(key: String = Keys.nextKey) extends StdElement
-case class Em(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, String] = Map.empty) extends StdElement
+case class Span(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement
+case class NewLine(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends StdElement
+case class Em(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement
-case class Header1(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, String] = Map.empty) extends StdElement
+case class Header1(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement
case class Paragraph(
key: String = Keys.nextKey,
@volatile var text: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends StdElement
with HasChildren[Paragraph]
@@ -23,7 +23,7 @@ case class Input(
key: String = Keys.nextKey,
`type`: String = "text",
defaultValue: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var value: String = ""
) extends StdElement
with HasEventHandler:
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala
new file mode 100644
index 00000000..c46bec4e
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala
@@ -0,0 +1,61 @@
+package org.terminal21.client.components
+
+import functions.fibers.FiberExecutor
+import org.terminal21.client.ConnectedSession
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.chakra.*
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the
+ * calculation completes, it allows for updating the dataUi component.
+ * @tparam OUT
+ * the return value of the calculation.
+ */
+trait StdUiCalculation[OUT](
+ name: String,
+ dataUi: UiElement with HasStyle
+)(using session: ConnectedSession, executor: FiberExecutor)
+ extends Calculation[OUT]
+ with UiComponent:
+ val badge = Badge()
+ private val running = new AtomicBoolean(false)
+ val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () =>
+ if running.compareAndSet(false, true) then
+ try
+ reCalculate()
+ finally running.set(false)
+
+ val header = Box(bg = "green", p = 4).withChildren(
+ HStack().withChildren(
+ Text(text = name),
+ badge,
+ recalc
+ )
+ )
+ @volatile var children: Seq[UiElement] = Seq(
+ header,
+ dataUi
+ )
+
+ override def onError(t: Throwable): Unit =
+ badge.text = s"Error: ${t.getMessage}"
+ badge.colorScheme = Some("red")
+ recalc.isDisabled = None
+ session.render()
+ super.onError(t)
+
+ override protected def whenResultsNotReady(): Unit =
+ badge.text = "Calculating"
+ badge.colorScheme = Some("purple")
+ recalc.isDisabled = Some(true)
+ dataUi.style = dataUi.style + ("filter" -> "grayscale(100%)")
+ session.render()
+ super.whenResultsNotReady()
+
+ override protected def whenResultsReady(results: OUT): Unit =
+ badge.text = "Ready"
+ badge.colorScheme = None
+ recalc.isDisabled = Some(false)
+ dataUi.style = dataUi.style - "filter"
+ session.render()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala
new file mode 100644
index 00000000..bf121453
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala
@@ -0,0 +1,7 @@
+package org.terminal21.client.components
+
+import org.terminal21.client.components.UiElement.HasChildren
+
+/** A UiComponent is a UI element that is composed of a seq of other ui elements
+ */
+trait UiComponent extends UiElement with HasChildren[UiComponent]
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
index 9d63a2ad..eca8cb17 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
@@ -30,3 +30,6 @@ object UiElement:
trait HasEventHandler:
def defaultEventHandler: EventHandler
+
+ trait HasStyle:
+ var style: Map[String, Any]
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala
index 6ea4a877..1d312f19 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala
@@ -3,9 +3,34 @@ package org.terminal21.client.components
import io.circe.*
import io.circe.generic.auto.*
import io.circe.syntax.*
-import org.terminal21.client.components.chakra.ChakraElement
+import org.terminal21.client.components.chakra.{Box, ChakraElement}
-object UiElementEncoding:
+class UiElementEncoding(libs: Seq[ComponentLib]):
given uiElementEncoder: Encoder[UiElement] =
+ a =>
+ val cl =
+ libs
+ .find(_.toJson.isDefinedAt(a))
+ .getOrElse(throw new IllegalStateException(s"Unknown ui element, did you forget to register a Lib when creating a session? Component: $a"))
+ cl.toJson(a)
+
+object StdElementEncoding extends ComponentLib:
+ given Encoder[Map[String, Any]] = m =>
+ val vs = m.toSeq.map: (k, v) =>
+ (
+ k,
+ v match
+ case s: String => Json.fromString(s)
+ case i: Int => Json.fromInt(i)
+ case f: Float => Json.fromFloat(f).get
+ case d: Double => Json.fromDouble(d).get
+ case _ => throw new IllegalArgumentException(s"type $v not supported, either use one of the supported ones or open a bug request")
+ )
+ Json.obj(vs: _*)
+
+ override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
case std: StdElement => std.asJson.mapObject(o => o.add("type", "Std".asJson))
case c: ChakraElement => c.asJson.mapObject(o => o.add("type", "Chakra".asJson))
+ case c: UiComponent =>
+ val b: ChakraElement = Box(children = c.children)
+ b.asJson.mapObject(o => o.add("type", "Chakra".asJson))
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
index 75284446..54be2d48 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
@@ -1,10 +1,14 @@
package org.terminal21.client.components.chakra
-import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler}
+import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle}
import org.terminal21.client.components.{Keys, UiElement}
import org.terminal21.client.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler}
-sealed trait ChakraElement extends UiElement
+/** The chakra-react based components, for a complete (though bit rough) example please see
+ * https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under
+ * https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra
+ */
+sealed trait ChakraElement extends UiElement with HasStyle
/** https://chakra-ui.com/docs/components/button
*/
@@ -14,7 +18,14 @@ case class Button(
@volatile var size: Option[String] = None,
@volatile var variant: Option[String] = None,
@volatile var colorScheme: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty,
+ @volatile var leftIcon: Option[UiElement] = None,
+ @volatile var rightIcon: Option[UiElement] = None,
+ @volatile var isActive: Option[Boolean] = None,
+ @volatile var isDisabled: Option[Boolean] = None,
+ @volatile var isLoading: Option[Boolean] = None,
+ @volatile var isAttached: Option[Boolean] = None,
+ @volatile var spacing: Option[String] = None
) extends ChakraElement
with OnClickEventHandler.CanHandleOnClickEvent[Button]
@@ -29,7 +40,7 @@ case class ButtonGroup(
@volatile var height: Option[String] = None,
@volatile var border: Option[String] = None,
@volatile var borderColor: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[ButtonGroup]
@@ -43,7 +54,8 @@ case class Box(
@volatile var w: String = "",
@volatile var p: Int = 0,
@volatile var color: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
+ @volatile var as: Option[String] = None,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[Box]
@@ -54,7 +66,7 @@ case class HStack(
key: String = Keys.nextKey,
@volatile var spacing: Option[String] = None,
@volatile var align: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[HStack]
@@ -62,7 +74,7 @@ case class VStack(
key: String = Keys.nextKey,
@volatile var spacing: Option[String] = None,
@volatile var align: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[VStack]
@@ -74,7 +86,7 @@ case class SimpleGrid(
@volatile var spacingY: Option[String] = None,
@volatile var columns: Int = 2,
@volatile var children: Seq[UiElement] = Nil,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[SimpleGrid]
@@ -84,7 +96,7 @@ case class Editable(
key: String = Keys.nextKey,
defaultValue: String = "",
@volatile var value: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasEventHandler
@@ -93,16 +105,16 @@ case class Editable(
if value == "" then value = defaultValue
override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue
-case class EditablePreview(key: String = Keys.nextKey, @volatile var style: Map[String, String] = Map.empty) extends ChakraElement
-case class EditableInput(key: String = Keys.nextKey, @volatile var style: Map[String, String] = Map.empty) extends ChakraElement
-case class EditableTextarea(key: String = Keys.nextKey, @volatile var style: Map[String, String] = Map.empty) extends ChakraElement
+case class EditablePreview(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement
+case class EditableInput(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement
+case class EditableTextarea(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement
/** https://chakra-ui.com/docs/components/form-control
*/
case class FormControl(
key: String = Keys.nextKey,
as: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[FormControl]
@@ -112,7 +124,7 @@ case class FormControl(
case class FormLabel(
key: String = Keys.nextKey,
@volatile var text: String,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[FormLabel]
@@ -122,7 +134,7 @@ case class FormLabel(
case class FormHelperText(
key: String = Keys.nextKey,
@volatile var text: String,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[FormHelperText]
@@ -136,7 +148,7 @@ case class Input(
@volatile var size: String = "md",
@volatile var variant: Option[String] = None,
@volatile var value: String = "",
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasEventHandler
with OnChangeEventHandler.CanHandleOnChangeEvent[Input]:
@@ -145,7 +157,7 @@ case class Input(
case class InputGroup(
key: String = Keys.nextKey,
@volatile var size: String = "md",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[InputGroup]
@@ -153,7 +165,7 @@ case class InputGroup(
case class InputLeftAddon(
key: String = Keys.nextKey,
@volatile var text: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[InputLeftAddon]
@@ -161,7 +173,7 @@ case class InputLeftAddon(
case class InputRightAddon(
key: String = Keys.nextKey,
@volatile var text: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[InputRightAddon]
@@ -173,7 +185,7 @@ case class Checkbox(
@volatile var text: String = "",
defaultChecked: Boolean = false,
@volatile var isDisabled: Boolean = false,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasEventHandler
with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]:
@@ -183,13 +195,18 @@ case class Checkbox(
/** https://chakra-ui.com/docs/components/radio
*/
-case class Radio(key: String = Keys.nextKey, value: String, @volatile var text: String = "", @volatile var colorScheme: Option[String] = None)
- extends ChakraElement
+case class Radio(
+ key: String = Keys.nextKey,
+ value: String,
+ @volatile var text: String = "",
+ @volatile var colorScheme: Option[String] = None,
+ @volatile var style: Map[String, Any] = Map.empty
+) extends ChakraElement
case class RadioGroup(
key: String = Keys.nextKey,
defaultValue: String = "",
@volatile var value: String = "",
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasEventHandler
@@ -207,7 +224,7 @@ case class Center(
@volatile var w: Option[String] = None,
@volatile var h: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Center]
@@ -219,7 +236,7 @@ case class Circle(
@volatile var w: Option[String] = None,
@volatile var h: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Circle]
@@ -231,7 +248,7 @@ case class Square(
@volatile var w: Option[String] = None,
@volatile var h: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Square]
@@ -243,7 +260,7 @@ case class AddIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -254,7 +271,7 @@ case class ArrowBackIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -265,7 +282,7 @@ case class ArrowDownIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -276,7 +293,7 @@ case class ArrowForwardIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -287,7 +304,7 @@ case class ArrowLeftIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -298,7 +315,7 @@ case class ArrowRightIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -309,7 +326,7 @@ case class ArrowUpIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -320,7 +337,7 @@ case class ArrowUpDownIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -331,7 +348,7 @@ case class AtSignIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -342,7 +359,7 @@ case class AttachmentIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -353,7 +370,7 @@ case class BellIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -364,7 +381,7 @@ case class CalendarIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -375,7 +392,7 @@ case class ChatIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -386,7 +403,7 @@ case class CheckIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -397,7 +414,7 @@ case class CheckCircleIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -408,7 +425,7 @@ case class ChevronDownIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -419,7 +436,7 @@ case class ChevronLeftIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -430,7 +447,7 @@ case class ChevronRightIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -441,7 +458,7 @@ case class ChevronUpIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -452,7 +469,7 @@ case class CloseIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -463,7 +480,7 @@ case class CopyIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -474,7 +491,7 @@ case class DeleteIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -485,7 +502,7 @@ case class DownloadIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -496,7 +513,7 @@ case class DragHandleIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -507,7 +524,7 @@ case class EditIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -518,7 +535,7 @@ case class EmailIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -529,7 +546,7 @@ case class ExternalLinkIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -540,7 +557,7 @@ case class HamburgerIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -551,7 +568,7 @@ case class InfoIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -562,7 +579,7 @@ case class InfoOutlineIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -573,7 +590,7 @@ case class LinkIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -584,7 +601,7 @@ case class LockIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -595,7 +612,7 @@ case class MinusIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -606,7 +623,7 @@ case class MoonIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -617,7 +634,7 @@ case class NotAllowedIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -628,7 +645,7 @@ case class PhoneIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -639,7 +656,7 @@ case class PlusSquareIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -650,7 +667,7 @@ case class QuestionIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -661,7 +678,7 @@ case class QuestionOutlineIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -672,7 +689,7 @@ case class RepeatIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -683,7 +700,7 @@ case class RepeatClockIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -694,7 +711,7 @@ case class SearchIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -705,7 +722,7 @@ case class Search2Icon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -716,7 +733,7 @@ case class SettingsIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -727,7 +744,7 @@ case class SmallAddIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -738,7 +755,7 @@ case class SmallCloseIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -749,7 +766,7 @@ case class SpinnerIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -760,7 +777,7 @@ case class StarIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -771,7 +788,7 @@ case class SunIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -782,7 +799,7 @@ case class TimeIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -793,7 +810,7 @@ case class TriangleDownIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -804,7 +821,7 @@ case class TriangleUpIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -815,7 +832,7 @@ case class UnlockIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -826,7 +843,7 @@ case class UpDownIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -837,7 +854,7 @@ case class ViewIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -848,7 +865,7 @@ case class ViewOffIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -859,7 +876,7 @@ case class WarningIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
@@ -870,7 +887,7 @@ case class WarningTwoIcon(
@volatile var h: Option[String] = None,
@volatile var boxSize: Option[String] = None,
@volatile var color: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** https://chakra-ui.com/docs/components/textarea
@@ -882,7 +899,7 @@ case class Textarea(
@volatile var size: String = "md",
@volatile var variant: Option[String] = None,
@volatile var value: String = "",
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasEventHandler
with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]:
@@ -895,7 +912,7 @@ case class Switch(
@volatile var text: String = "",
defaultChecked: Boolean = false,
@volatile var isDisabled: Boolean = false,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasEventHandler
with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]:
@@ -908,55 +925,69 @@ case class Switch(
case class Select(
key: String = Keys.nextKey,
placeholder: String = "",
- defaultValue: String = "",
@volatile var value: String = "",
@volatile var bg: Option[String] = None,
@volatile var color: Option[String] = None,
@volatile var borderColor: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasEventHandler
with HasChildren[Select]
with OnChangeEventHandler.CanHandleOnChangeEvent[Select]:
- if value == "" then value = defaultValue
-
override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue
case class Option_(
key: String = Keys.nextKey,
value: String,
@volatile var text: String = "",
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
/** https://chakra-ui.com/docs/components/table/usage
*/
-case class TableContainer(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren[TableContainer]
+case class TableContainer(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty)
+ extends ChakraElement
+ with HasChildren[TableContainer]:
+ def withRowStringData(data: Seq[Seq[String]]): TableContainer = withRowData(data.map(_.map(c => Text(text = c))))
+ def withRowData(data: Seq[Seq[UiElement]]): TableContainer =
+ val tableBodies = children
+ .collect:
+ case t: Table =>
+ t.children.collect:
+ case b: Tbody => b
+ .flatten
+ val newTrs = data.map: row =>
+ Tr(
+ children = row.map: column =>
+ Td().withChildren(column)
+ )
+ for b <- tableBodies do b.withChildren(newTrs: _*)
+ this
+
case class Table(
key: String = Keys.nextKey,
@volatile var variant: String = "simple",
@volatile var size: String = "md",
@volatile var colorScheme: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[Table]
-case class TableCaption(key: String = Keys.nextKey, @volatile var text: String = "") extends ChakraElement
-case class Thead(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, String] = Map.empty)
+case class TableCaption(key: String = Keys.nextKey, @volatile var text: String = "", @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement
+case class Thead(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty)
extends ChakraElement
with HasChildren[Thead]
-case class Tbody(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, String] = Map.empty)
+case class Tbody(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty)
extends ChakraElement
with HasChildren[Tbody]
-case class Tfoot(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, String] = Map.empty)
+case class Tfoot(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty)
extends ChakraElement
with HasChildren[Tfoot]
case class Tr(
key: String = Keys.nextKey,
- isNumeric: Boolean = false,
@volatile var children: Seq[UiElement] = Nil,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Tr]
case class Th(
@@ -964,31 +995,45 @@ case class Th(
@volatile var text: String = "",
isNumeric: Boolean = false,
@volatile var children: Seq[UiElement] = Nil,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Th]
-case class Td(key: String = Keys.nextKey, @volatile var text: String = "", isNumeric: Boolean = false, @volatile var children: Seq[UiElement] = Nil)
- extends ChakraElement
+case class Td(
+ key: String = Keys.nextKey,
+ @volatile var text: String = "",
+ isNumeric: Boolean = false,
+ @volatile var style: Map[String, Any] = Map.empty,
+ @volatile var children: Seq[UiElement] = Nil
+) extends ChakraElement
with HasChildren[Td]
/** https://chakra-ui.com/docs/components/menu/usage
*/
-case class Menu(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren[Menu]
+case class Menu(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty, @volatile var children: Seq[UiElement] = Nil)
+ extends ChakraElement
+ with HasChildren[Menu]
case class MenuButton(
key: String = Keys.nextKey,
@volatile var text: String = "",
@volatile var size: Option[String] = None,
@volatile var colorScheme: Option[String] = None,
+ @volatile var style: Map[String, Any] = Map.empty,
@volatile var children: Seq[UiElement] = Nil
) extends ChakraElement
with HasChildren[MenuButton]
-case class MenuList(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren[MenuList]
-case class MenuItem(key: String = Keys.nextKey, @volatile var text: String = "", @volatile var children: Seq[UiElement] = Nil)
+case class MenuList(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty, @volatile var children: Seq[UiElement] = Nil)
extends ChakraElement
+ with HasChildren[MenuList]
+case class MenuItem(
+ key: String = Keys.nextKey,
+ @volatile var style: Map[String, Any] = Map.empty,
+ @volatile var text: String = "",
+ @volatile var children: Seq[UiElement] = Nil
+) extends ChakraElement
with HasChildren[MenuItem]
with OnClickEventHandler.CanHandleOnClickEvent[MenuItem]
-case class MenuDivider(key: String = Keys.nextKey) extends ChakraElement
+case class MenuDivider(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement
case class Badge(
key: String = Keys.nextKey,
@@ -997,7 +1042,7 @@ case class Badge(
@volatile var variant: Option[String] = None,
@volatile var size: String = "md",
@volatile var children: Seq[UiElement] = Nil,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
with HasChildren[Badge]
@@ -1013,5 +1058,20 @@ case class Image(
@volatile var alt: String = "",
@volatile var boxSize: Option[String] = None,
@volatile var borderRadius: Option[String] = None,
- @volatile var style: Map[String, String] = Map.empty
+ @volatile var style: Map[String, Any] = Map.empty
+) extends ChakraElement
+
+/** https://chakra-ui.com/docs/components/text
+ */
+case class Text(
+ key: String = Keys.nextKey,
+ @volatile var text: String = "text.text is empty. Did you accidentally assigned the text to the `key` field?",
+ @volatile var fontSize: Option[String] = None,
+ @volatile var noOfLines: Option[Int] = None,
+ @volatile var color: Option[String] = None,
+ @volatile var as: Option[String] = None,
+ @volatile var align: Option[String] = None,
+ @volatile var casing: Option[String] = None,
+ @volatile var decoration: Option[String] = None,
+ @volatile var style: Map[String, Any] = Map.empty
) extends ChakraElement
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala
new file mode 100644
index 00000000..fb7dd571
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala
@@ -0,0 +1,41 @@
+package org.terminal21.client.components.chakra
+
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiComponent, UiElement}
+
+case class QuickTable(
+ key: String = Keys.nextKey,
+ variant: String = "striped",
+ colorScheme: String = "teal",
+ size: String = "mg"
+) extends UiComponent
+ with HasStyle:
+ val head = Thead()
+ val body = Tbody()
+
+ val table = Table(variant = variant, colorScheme = Some(colorScheme), size = size)
+ .withChildren(
+ head,
+ body
+ )
+ @volatile var tableContainer = TableContainer().withChildren(table)
+ @volatile var children: Seq[UiElement] = Seq(tableContainer)
+
+ def headers(headers: String*): QuickTable = headersElements(headers.map(h => Text(text = h)): _*)
+ def headersElements(headers: UiElement*): QuickTable =
+ head.children = headers.map(h => Th(children = Seq(h)))
+ this
+
+ def rows(data: Seq[Seq[Any]]): QuickTable = rowsElements(data.map(_.map(c => Text(text = c.toString))))
+
+ def rowsElements(data: Seq[Seq[UiElement]]): QuickTable =
+ body.children = data.map: row =>
+ Tr(children = row.map(c => Td().withChildren(c)))
+ this
+
+ def style: Map[String, Any] = tableContainer.style
+ def style_=(s: Map[String, Any]): Unit = tableContainer.style = s
+
+ def caption(text: String): QuickTable =
+ table.addChildren(TableCaption(text = text))
+ this
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala
new file mode 100644
index 00000000..9fea22ac
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala
@@ -0,0 +1,37 @@
+package org.terminal21.client
+
+import functions.fibers.FiberExecutor
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+
+import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
+import org.scalatest.concurrent.Eventually.*
+import org.terminal21.client.components.Calculation
+
+class CalculationTest extends AnyFunSuiteLike:
+ given executor: FiberExecutor = FiberExecutor()
+ def testCalc(i: Int) = i + 1
+ def testCalcString(i: Int): String = (i + 10).toString
+
+ class Calc extends Calculation[Int]:
+ val whenResultsNotReadyCalled = new AtomicBoolean(false)
+ val whenResultsReadyValue = new AtomicInteger(-1)
+ override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true)
+ override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results)
+ override protected def calculation() = 2
+
+ test("calculates"):
+ val calc = new Calc
+ calc.run().get() should be(2)
+
+ test("calls whenResultsNotReady"):
+ val calc = new Calc
+ calc.run()
+ eventually:
+ calc.whenResultsNotReadyCalled.get() should be(true)
+
+ test("calls whenResultsReady"):
+ val calc = new Calc
+ calc.run()
+ eventually:
+ calc.whenResultsReadyValue.get() should be(2)
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala
new file mode 100644
index 00000000..ca667af7
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala
@@ -0,0 +1,11 @@
+package org.terminal21.client
+
+import org.mockito.Mockito.mock
+import org.terminal21.client.components.{StdElementEncoding, UiElementEncoding}
+import org.terminal21.model.CommonModelBuilders.session
+import org.terminal21.ui.std.SessionsService
+
+object ConnectedSessionMock:
+ def newConnectedSessionMock: ConnectedSession =
+ val sessionsService = mock(classOf[SessionsService])
+ new ConnectedSession(session(), new UiElementEncoding(Seq(StdElementEncoding)), "test", sessionsService, () => ())
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
index 0d5a423e..42706244 100644
--- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
@@ -1,17 +1,13 @@
package org.terminal21.client
-import org.mockito.Mockito.*
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers.*
import org.terminal21.client.components.chakra.Editable
-import org.terminal21.model.CommonModelBuilders.*
import org.terminal21.model.OnChange
-import org.terminal21.ui.std.SessionsService
class ConnectedSessionTest extends AnyFunSuiteLike:
test("default event handlers are invoked before user handlers"):
- val sessionsService = mock(classOf[SessionsService])
- given connectedSession: ConnectedSession = new ConnectedSession(session(), "test", sessionsService, () => ())
+ given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
val editable = Editable()
editable.onChange: newValue =>
editable.value should be(newValue)