New Solr Admin UI
UI Development with Kotlin and Compose
Before we start
Solr Improvement Proposal�https://cwiki.apache.org/confluence/display/SOLR/SIP-7+Updated+Solr+Admin+UI
Proof of Concept�https://github.com/apache/solr/pull/2605
New Admin UI Design Proposal (Figma)�https://www.figma.com/design/VdbEfcWQ8mirFNquBzbPk2/Apache-Solr-Admin-UI-v2-Concept
Discussion Topic (dev)�https://lists.apache.org/thread/knd5xgkswp83x92c0sclh7ghcob7bpfx
Table of Content
Languages, Libraries and Frameworks
From Java To Kotlin
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
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"
}
Visit https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html for more information.
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
Visit https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html for more information.
From Java To Kotlin - Asynchronous programming with Coroutines
Visit https://kotlinlang.org/docs/coroutines-overview.html for more details.
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
UI Libraries
UI Libraries
UI Libraries
Decompose
Essenty
MVIKotlin
UI Libraries
A Design system …
Theming, colors, shapes, typography, icons
Buttons, checkboxes, menus, sliders …
UI Components
Components in Detail
Components in Detail
Components in Detail
Developing a new Component
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,
)
}
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,
)
}
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,
)
}
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,
)
}
}
}
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,
)
}
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(),
)
}
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)
}
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
}
Composable
PreviewEnvironmentContent
Composable of a single java property that can be used in lists.
@Composable
fun PreviewEnvironmentContent() = PreviewContainer {
EnvironmentContent(
component = PreviewEnvironmentComponent,
modifier = Modifier.fillMaxSize(),
)
}
Composable
Desktop Preview of PreviewEnvironmentContent with PreviewEnvironmentComponent
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(),
)
}
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,
) {}
// ...
}
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
}
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() {...}
}
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
}
}
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,
)
}
}
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>>
}
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>> {...}
}
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)
}
}
Topics Left Out
Consider Contributing
Thank You
A presentation created by Christos Malliaridis�for the Apache Solr Project and Community