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