1 of 40

New Solr Admin UI

UI Development with Kotlin and Compose

2 of 40

Before we start

3 of 40

Table of Content

  • Languages, Libraries and Frameworks
    • From Java to Kotlin
    • UI Libraries
  • UI Components
    • Components in Detail
    • Developing a new Component

4 of 40

Languages, Libraries and Frameworks

5 of 40

From Java To Kotlin

  • Scratching the Surface
  • Kotlin Multiplatform
  • Multiplatform from Gradle’s perspective
  • Asynchronous programming with Coroutines

6 of 40

From Java To Kotlin - Scratching the Surface

data class Customer(

val name: String,

val email: String,

)

DTOs (POJOs/POCOs)

fun parseInt(str: String): Int? {

// ...

}

Explicit nullability

fun String.spaceToCamelCase() { ... }

​

"Convert this to camelcase".spaceToCamelCase()

Extension functions

Visit https://kotlinlang.org/ for a proper Getting Started experience.

// program entry point

fun main() {

println("Hello world!")

}

Functional Programming

7 of 40

From Java To Kotlin - Kotlin Multiplatform

// in common source set

expect fun platform(): String

// on JVM source set

actual fun platform(): String {

return "JVM"

}

// on WASM source set

actual fun platform(): String {

return "WASM"

}

8 of 40

From Java To Kotlin - Multiplatform from Gradle’s perspective

// shared/build.gradle.kts

kotlin {

jvm() // JVM target

iosArm64() // 64-bit iPhones

//...

​

// Source set declaration:

sourceSets {

commonMain {

// common main source set

}

jvmMain {

// configure the jvm main source set

}

}

}

https://kotlinlang.org/docs/multiplatform-discover-project.html

9 of 40

From Java To Kotlin - Asynchronous programming with Coroutines

fun main() = runBlocking { // this: CoroutineScope

launch { doWorld() }

println("Hello")

}

​

// suspending function

suspend fun doWorld() {

delay(1000L)

println("World!")

}

10 of 40

UI Libraries

  • Compose MP
  • Decompose
  • MVIKotlin
  • Essenty
  • Material

11 of 40

UI Libraries

  • Compose MP
  • Decompose
  • MVIKotlin
  • Essenty
  • Material

12 of 40

UI Libraries

  • Compose MP
  • Decompose
  • Essenty
  • MVIKotlin
  • Material

Decompose

Essenty

MVIKotlin

13 of 40

UI Libraries

  • Compose MP
  • Decompose
  • Essenty
  • MVIKotlin
  • Material

A Design system …

​

  • defines styles

Theming, colors, shapes, typography, icons

​

  • provides customizable UI elements

Buttons, checkboxes, menus, sliders …

​

  • takes accessibility seriously

​

  • clarifies layouting

14 of 40

UI Components

15 of 40

Components in Detail

  • Component Structure
  • Component Lifecycle
  • State Management

16 of 40

Components in Detail

  • Component Structure
  • Component Lifecycle
  • State Management

17 of 40

Components in Detail

  • Component Structure
  • Component Lifecycle
  • State Management

18 of 40

Developing a new Component

  • Component Interface
  • Component Implementation
  • Composable
  • Store and API
  • Testing

19 of 40

Composable

EnvironmentContent

Composable for loading the environment section.

This composable checks the window size and rearranges the content to achieve a better representation.

​

@Composable

fun EnvironmentContent(

component: EnvironmentComponent,

modifier: Modifier = Modifier,

) {

val model by component.model.collectAsState()

val windowSizeClass = calculateWindowSizeClass()

val isLargeScreen =

windowSizeClass.widthSizeClass > WindowWidthSizeClass.Medium

​

if (isLargeScreen) EnvironmentContentExpanded(

modifier = modifier,

model = model,

) else EnvironmentContentMedium(

modifier = modifier,

model = model,

)

}

20 of 40

Composable

EnvironmentContentExpanded

Composable for loading the environment section for expanded window sizes.

@Composable

private fun EnvironmentContentExpanded(

model: EnvironmentComponent.Model,

modifier: Modifier = Modifier,

) = Row(

modifier = modifier,

horizontalArrangement = Arrangement.spacedBy(16.dp),

) {

Column(

modifier = Modifier.weight(1f),

verticalArrangement = Arrangement.spacedBy(16.dp),

) {

VersionsCard(

modifier = Modifier.fillMaxWidth(),

versions = model.versions,

jvm = model.jvm,

)

JavaPropertiesCard(

modifier = Modifier.fillMaxWidth(),

properties = model.javaProperties,

)

}

CommandLineArgumentsCard(

modifier = Modifier.weight(1f),

arguments = model.jvm.jmx.commandLineArgs,

)

}

21 of 40

Composable

EnvironmentContentMedium

Composable for loading the environment section for medium window sizes.

@Composable

private fun EnvironmentContentMedium(

model: EnvironmentComponent.Model,

modifier: Modifier = Modifier,

) = Column(

modifier = modifier,

verticalArrangement = Arrangement.spacedBy(16.dp),

) {

VersionsCard(

modifier = Modifier.fillMaxWidth(),

versions = model.versions,

jvm = model.jvm,

)

JavaPropertiesCard(

modifier = Modifier.fillMaxWidth(),

properties = model.javaProperties,

)

CommandLineArgumentsCard(

modifier = Modifier.fillMaxWidth(),

arguments = model.jvm.jmx.commandLineArgs,

)

}

22 of 40

Composable

JavaPropertiesCard

Composable card that displays the provided java properties.

@Composable

internal fun JavaPropertiesCard(

properties: List<JavaProperty>,

modifier: Modifier = Modifier,

) = SolrCard(

modifier = modifier,

verticalArrangement = Arrangement.spacedBy(16.dp)

) {

Text(

text = "Java Properties",

style = MaterialTheme.typography.headlineSmall,

color = MaterialTheme.colorScheme.onSurfaceVariant,

)

Column(

modifier = Modifier.fillMaxWidth()

.border(

BorderStroke(

1.dp, MaterialTheme.colorScheme.outlineVariant

)

),

) {

properties.forEachIndexed { index, property ->

JavaPropertyEntry(

property = property,

isOdd = index % 2 == 0,

)

}

}

}

23 of 40

Composable

JavaPropertyEntry

Composable of a single java property that can be used in lists.

@Composable

private fun JavaPropertyEntry(

property: JavaProperty,

modifier: Modifier = Modifier,

isOdd: Boolean = false,

) = Row(

modifier = modifier.background(

MaterialTheme.colorScheme.surfaceColorAtElevation(

if (isOdd) 1.dp else 0.dp,

)

).padding(horizontal = 8.dp, vertical = 4.dp),

) {

Text(

modifier = Modifier.weight(1f),

text = property.first,

style = SolrTheme.typography.codeLarge,

color = MaterialTheme.colorScheme.onSurfaceVariant,

)

Text(

modifier = Modifier.weight(1f),

text = property.second,

style = SolrTheme.typography.codeLarge,

color = MaterialTheme.colorScheme.onSurface,

)

}

24 of 40

Component Interface

EnvironmentComponent

Component interface that represents the environment section.

Note that the “onRefreshClicked()” function was added only for demonstration reasons here.

interface EnvironmentComponent {

​

val model: StateFlow<Model>

​

fun onRefreshClicked()

​

data class Model(

val versions: Versions = Versions(),

val jvm: JvmData = JvmData(),

val javaProperties: List<JavaProperty> = emptyList(),

)

}

25 of 40

Component Implementation

DefaultEnvironmentComponent

Default implementation of the [EnvironmentComponent].

class DefaultEnvironmentComponent(

componentContext: AppComponentContext,

storeFactory: StoreFactory,

httpClient: HttpClient,

) : EnvironmentComponent,

AppComponentContext by componentContext {

​

private val mainScope = coroutineScope(mainContext)

​

private val store = instanceKeeper.getStore {

EnvironmentStoreProvider(

storeFactory = storeFactory,

client = HttpEnvironmentStoreClient(httpClient),

ioContext = ioContext,

).provide()

}

​

override fun onRefreshClicked() =

store.accept(Intent.FetchSystemData)

​

@OptIn(ExperimentalCoroutinesApi::class)

override val model =

store.stateFlow.map(mainScope, environmentStateToModel)

}

26 of 40

Component Implementation

PreviewEnvironmentComponent

Preview implementation of the EnvironmentComponent that populates the state with dummy data.

private object PreviewEnvironmentComponent:

EnvironmentComponent {

​

override val model: StateFlow<EnvironmentComponent.Model> =

MutableStateFlow(

EnvironmentComponent.Model(

versions = Versions(

solrSpecVersion = "10.0",

solrImplVersion = "10.0.0-compose-build",

luceneSpecVersion = "10.0",

luceneImplVersion = "10.0.0-compose-build",

),

jvm = JvmData(

version = "11.0.23",

//...

jmx = Jmx(

commandLineArgs = listOf("-Dpreview.argument"),

),

),

javaProperties = listOf(

"private.key" to "custom value"

),

)

)

​

override fun onRefreshClicked() = Unit

}

27 of 40

Composable

PreviewEnvironmentContent

Composable of a single java property that can be used in lists.

@Composable

fun PreviewEnvironmentContent() = PreviewContainer {

EnvironmentContent(

component = PreviewEnvironmentComponent,

modifier = Modifier.fillMaxSize(),

)

}

28 of 40

Composable

Desktop Preview of PreviewEnvironmentContent with PreviewEnvironmentComponent

​

29 of 40

Store and API

EnvironmentStore

State store interface of the environment.

Implementations of this state store manage detailed information of the environment.

internal interface EnvironmentStore :

Store<Intent, State, Nothing> {

​

sealed interface Intent {

​

data object FetchSystemData: Intent

}

​

data class State(

val mode: SystemMode = SystemMode.Unknown,

val zkHost: String = "",

val solrHome: String = "",

val coreRoot: String = "",

val lucene: Versions = Versions(),

val jvm: JvmData = JvmData(),

val security: SecurityConfig = SecurityConfig(),

val system: SystemInformation = SystemInformation(),

val node: String = "",

val javaProperties: List<JavaProperty> = emptyList(),

)

}

30 of 40

Store and API

EnvironmentStoreProvider

Store provider that [provide]s instances of [EnvironmentStore].

internal class EnvironmentStoreProvider(

private val storeFactory: StoreFactory,

private val client: Client,

private val ioContext: CoroutineContext,

) {

​

fun provide(): EnvironmentStore = object :

EnvironmentStore,

Store<Intent, State, Nothing>

by storeFactory.create(

name = "EnvironmentStore",

initialState = State(),

bootstrapper = SimpleBootstrapper(

Action.FetchInitialSystemData

),

executorFactory = ::ExecutorImpl,

reducer = ReducerImpl,

) {}

// ...

}

31 of 40

Store and API

ESProvider.Action

Store provider that [provide]s instances of [EnvironmentStore].

private sealed interface Action {

​

/**

* Action used for initiating the initial fetch of environment data.

*/

data object FetchInitialSystemData: Action

}

32 of 40

Store and API

ESProvider.ExecutorImpl

private inner class ExecutorImpl :

CoroutineExecutor<Intent, Action, State, Message, Nothing>() {

​

override fun executeAction(action: Action) = when(action) {

Action.FetchInitialSystemData -> {

fetchSystemData()

fetchJavaProperties()

}

}

​

override fun executeIntent(intent: Intent) {

when (intent) {

Intent.FetchSystemData -> {

fetchSystemData()

fetchJavaProperties()

}

}

}

​

private fun fetchSystemData() {...}

private fun fetchJavaProperties() {...}

}

33 of 40

Store and API

ESProvider.ExecutorImpl�#fetchSystemData()

Fetches the system data that are part of the environment state.

If successful, a [Message.SystemDataUpdated] with the new properties is dispatched.

private fun fetchSystemData() {

scope.launch { // TODO Add coroutine exception handler

withContext(ioContext) {

client.getSystemData()

}.onSuccess {

dispatch(Message.SystemDataUpdated(it))

}

​

withContext(ioContext) {

client.getJavaProperties()

}.onSuccess {

dispatch(Message.JavaPropertiesUpdated(it))

}

// TODO Add error handling

}

}

34 of 40

Store and API

ESProvider.ReducerImpl

Reducer implementation that consumes [Message]s and updates the store's [State].

private object ReducerImpl : Reducer<State, Message> {

override fun State.reduce(msg: Message): State =

when (msg) {

is Message.SystemDataUpdated -> copy(

mode = msg.data.mode,

zkHost = msg.data.zkHost,

solrHome = msg.data.solrHome,

coreRoot = msg.data.coreRoot,

lucene = msg.data.lucene,

jvm = msg.data.jvm,

security = msg.data.security,

system = msg.data.system,

node = msg.data.node,

)

is Message.JavaPropertiesUpdated -> copy(

javaProperties = msg.properties,

)

}

}

35 of 40

Store and API

ESProvider.Client

Client interface for fetching environment information.

interface Client {

​

/**

* Fetches a set of system data.

*

* @return Result with the system data fetched.

*/

suspend fun getSystemData(): Result<SystemData>

​

/**

* Fetches the configured java properties.

*

* @return Result with a list of [JavaProperty]s.

*/

suspend fun getJavaProperties(): Result<List<JavaProperty>>

}

36 of 40

Store and API

HttpEnvironmentStoreClient

Client implementation of the [EnvironmentStoreProvider.Client] that makes use of a preconfigured HTTP client for accessing the Solr API.

class HttpEnvironmentStoreClient(

private val httpClient: HttpClient,

) : EnvironmentStoreProvider.Client {

​

override suspend fun getSystemData(): Result<SystemData> {

val response = httpClient.get("api/node/system")

​

return when {

response.status.isSuccess() ->

Result.success(response.body())

else -> Result.failure(Exception("Unknown error"))

// TODO Add proper error handling

}

}

​

override suspend fun getJavaProperties(): Result<List<JavaProperty>> {...}

}

37 of 40

Testing

HttpEnvironmentStoreClientTest

Dummy test for demonstration purpose that shows how individual components can be tested separately.

class HttpEnvironmentStoreClientTest {

​

@Test

fun simpleFetchCallsHttpEndpoint() = runTest {

val dummyResponse = SystemData(...)

val mockEngine = MockEngine { request ->

respond(

content = ByteReadChannel(

Json.encodeToString(dummyResponse)

),

status = HttpStatusCode.OK,

headers = headersOf(

HttpHeaders.ContentType, "application/json",

)

)

}

​

val storeClient = HttpEnvironmentStoreClient(

httpClient = getHttpTestClient(mockEngine),

)

val result = storeClient.getSystemData()

val response = result.getOrElse {

fail("Expected successful result received")

}

assertEquals(dummyResponse, response)

}

}

38 of 40

Topics Left Out

  • Navigation & Child Components
  • Application Entry Point & Root Component
  • Styling
  • Effects / Interactions
  • Special Components

39 of 40

Consider Contributing

40 of 40

Thank You

A presentation created by Christos Malliaridis�for the Apache Solr Project and Community