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)