You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@solr.apache.org by "iamsanjay (via GitHub)" <gi...@apache.org> on 2025/08/28 11:32:11 UTC

[PR] SOLR-17847: Introduce Navigation to Configsets [solr]

iamsanjay opened a new pull request, #3521:
URL: https://github.com/apache/solr/pull/3521

   https://issues.apache.org/jira/browse/SOLR-17847
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "malliaridis (via GitHub)" <gi...@apache.org>.
malliaridis commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2308649215


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(
+    selectedTab: ConfigsetsTab,
+    selectTab: (ConfigsetsTab) -> Unit,
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: ListConfigsets,
+    modifier: Modifier = Modifier,
+    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
+    },

Review Comment:
   With "[move out] to ConfigsetsComponent" you mean to `ConfigsetsContent` (composable)? If so, then yes, it makes sense to me. Otherwise you have to elaborate. :s 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "malliaridis (via GitHub)" <gi...@apache.org>.
malliaridis commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2321979502


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }

Review Comment:
   I would likely want to add this to the component state, so that when the user clicks on it, we can perform some tasks later, like "silent" refresh in the background (if that makes sense at some point). So the more is stored in the component, the more we are in control of user interactions.
   
   But we may also leave it as it is for now, and only update it later.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBar.kt:
##########
@@ -43,38 +43,32 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
-import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.data.Configset
 import org.apache.solr.ui.generated.resources.Res
-import org.apache.solr.ui.generated.resources.configsets_files
 import org.apache.solr.ui.generated.resources.configsets_index_query
-import org.apache.solr.ui.generated.resources.configsets_overview
-import org.apache.solr.ui.generated.resources.configsets_request_handlers
-import org.apache.solr.ui.generated.resources.configsets_schema
 import org.apache.solr.ui.generated.resources.configsets_search_components
 import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.generated.resources.files
+import org.apache.solr.ui.generated.resources.overview
+import org.apache.solr.ui.generated.resources.schema
 import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
 import org.jetbrains.compose.resources.stringResource
 
+
 @Composable
-fun ConfigsetsNavBarComponent(
+fun ConfigsetsNavBar(
     selectedTab: ConfigsetsTab,
     selectTab: (ConfigsetsTab) -> Unit,
-    selectedConfigSet: String,
-    selectConfigset: (String) -> Unit,
-    availableConfigsets: ListConfigsets,
+    availableConfigsets: List<Configset>,
     modifier: Modifier = Modifier,
-    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
-        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
-    },
 ) {
-    if (availableConfigsets.names.isEmpty()) {
+    if (availableConfigsets.isEmpty()) {
         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
             Text("No configsets available")
         }
         return
     }

Review Comment:
   I would normally move this logic to the parent (`ConfigsetsContent`), so that the NavBar does not have any reference to information it doesnt use. Render the `ConfigsetsNavBar` only if the availableConfigsets is not empty (in parent), otherwise render this box. And you may still add to the `ConfigsetComponent.Model` a state value like `hasConfigsets: boolean` that you can directly use (and populate during mapping of state to model), so that you don't need to run a function on a collection in the UI (even if it is a cheap operation here).
   
   Most important though is that I would not hide the navigation elements (UI/UX). Navigation elements in general should stay where they are to maintain recognizability (users should see the screen and directly know where they are by the static elements like navigation options). They should also know what they can do here, even for a fresh installation. Most you could do (and I would not even do that) is to disable the navigation options. But instead, I would support in each section the case where no configset is selected yet, so that user can still navigate through the tabs and see what the content is, before actually taking any actions or loading any content.
   
   When no configset is selected, the tab may display the sections with small hints (like "Here you can manage the configsets files. Select a configset to get started." for the Files tab). But that is out of scope of this PR.



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }
+    }
+
+    @Test
+    fun selectItem_updatesSelection_andClosesMenu() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(
+            names = listOf("gettingstarted", "techproducts"),
+            selected = "gettingstarted",
+        )
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Open
+        onNodeWithText("gettingstarted").assertExists().performClick()
+
+        // Select another
+        onNodeWithText("techproducts").assertExists().performClick()
+
+        // State updated + menu closed
+        runOnIdle {
+            assertEquals("techproducts", comp.model.value.selectedConfigset)
+            assertFalse(comp.model.value.expanded)
+        }
+    }
+
+    @Test
+    fun testSubsectionSwitch() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(names = listOf("basic_configs"))
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Click using the text node
+        onNodeWithText("Schema").assertExists().performClick()
+
+        runOnIdle { assertEquals(ConfigsetsTab.Schema, comp.model.value.selectedTab) }
+    }
+}
+
+private object DummyOverviewComponent : ConfigsetsOverviewComponent
+
+class TestConfigsetsComponent(
+    names: List<String>,
+    selected: String = "",
+    expanded: Boolean = false,
+) : ConfigsetsComponent {
+
+    private val _model = MutableStateFlow(
+        ConfigsetsComponent.Model(
+            selectedTab = ConfigsetsTab.Overview,
+            configsets = names.map(::Configset),
+            selectedConfigset = selected,
+            expanded = expanded,
+        ),
+    )
+    override val model: StateFlow<ConfigsetsComponent.Model> = _model

Review Comment:
   You may simplify your test component and your access in tests to your model values by not using a mutable state flow, but rather static fields where you can directly access. See for example [TestBasicAuthComponent](https://github.com/apache/solr/blob/main/solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/auth/TestBasicAuthComponent.kt). Of course the way you did it isn't wrong or bad, it is just less readable when accessing a field with `comp.model.value.selectedTab` instead of just `comp.selectedTab`. And you are close to start writing components that you have to test again due to the logic behind setting a field value and the relationship between fields. Imagine the case where you have `isValidEmail: Boolean` and `email: String` as state in your component. What would be the logic in your test component?



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }

Review Comment:
   To keep it simple with less nesting, prefer `advanceUntilIdle()`



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = 16.dp, vertical = 8.dp),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        ExposedDropdownMenuBox(
+            expanded = expanded,
+            onExpandedChange = { expanded = !expanded },
+            modifier = Modifier.widthIn(min = 256.dp).weight(1f),
+        ) {
+            OutlinedTextField(
+                value = selectedConfigSet,
+                onValueChange = {},
+                readOnly = true,
+                label = { Text("Configset") },

Review Comment:
   I believe there are still some values that can be localized?



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }
+    }
+
+    @Test
+    fun selectItem_updatesSelection_andClosesMenu() = runComposeUiTest {

Review Comment:
   You are mixing here UI test with integration test of your test component (instead of your actual component). It is hard to explain this, but let me try to break it down anyway.
   
   The statement "select item updates selection and closes menu" is a test that tests the behavior of your test component, and not testing the UI itself. Pure UI tests (that use the test component) would be:
   - WHEN dropdown clicked THEN setMenuExpanded is called
   - WHEN dropdown item clicked THEN onSelectConfigset called
   
   Possible integration tests (tests that use the actual implementation of ConfigsetsComponent) would be:
   - GIVEN expanded is true AND some configsets WHEN configset selected THEN dropdown collapses
   - GIVEN expanded is true AND some configsets WHEN configset selected THEN selectedConfigset changes
   - GIVEN expanded is false AND some configsets WHEN expand menu THEN expanded is true
   - GIVEN expanded is false AND no configsets WHEN expand menu THEN expanded is false
   
   Btw. the interface function `setMenuExpanded` could be renamed to `onExpandMenu` for better readability, both in tests and in code.
   
   I believe with these test examples it is a bit clearer where to draw the lines between simple UI tests, that test composable functions with test components, and actual integration tests, that use actual implementations. One of the root causes why it is easy to confuse these is that we are providing both business logic and UI in the same package. If we would use different packages, it would be easier to distinguish them. business logic does not know UI composables, and UI composables do not know implementation details of business logic. We may consider splitting the UI module into two modules in the future (if that makes sense to have two modules for the UI).



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()

Review Comment:
   When we later introduce localization, these tests may fail becasue the text may be localized during tests and not found via `onNodeWithText`. Therefore, it is recommended to use `Modifier.testTag(...)` in the composables to check if specific composables are present.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "malliaridis (via GitHub)" <gi...@apache.org>.
malliaridis commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2307802359


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.Value
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Contract for the Configsets feature.
+ *
+ * Responsibilities:
+ * - Owns tab/navigation state within the Configsets screen.
+ * - Loads and exposes the list of available configsets.
+ * - Persists the currently selected tab and configset.
+ *
+ * Exposed state:
+ * - [model]: reactive UI model (configset names, selected tab, selected configset).
+ *
+ * Actions:
+ * - [onSelectTab]: update the active tab.
+ * - [onSelectConfigset]: change the active configset.
+ *
+ * Notes:
+ * - Scope: feature-local only; app-level navigation is owned by [MainComponent].
+ * - State restoration is implementation-defined (e.g., MVIKotlin Store / Decompose StateKeeper).
+ *
+ * See also:
+ * - Decompose (component lifecycle & composition)
+ * - MVIKotlin (unidirectional data flow)
+ */
+interface ConfigsetsComponent {
+
+    /**
+     * Child stack that holds the navigation state.
+     */
+    val childStack: Value<ChildStack<*, Child>>
+
+    /**
+     * All possible navigation targets (children) within the Configsets feature.
+     *
+     * Each child wraps its own component, which holds the state and logic for that screen.

Review Comment:
   That is correct, but we don't have to mention that here (part of common knowledge).



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsConponent.kt:
##########
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets.integration
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.mvikotlin.core.instancekeeper.getStore
+import com.arkivanov.mvikotlin.core.store.StoreFactory
+import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.serialization.Serializable
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.components.configsets.overview.integration.DefaultOverviewComponent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
+import org.apache.solr.ui.utils.AppComponentContext
+import org.apache.solr.ui.utils.coroutineScope
+import org.apache.solr.ui.utils.map
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Default implementation of the [ConfigsetsComponent].
+ */
+class DefaultConfigsetsConponent internal constructor(
+    componentContext: AppComponentContext,
+    storeFactory: StoreFactory,
+    httpClient: HttpClient,
+    destination: String? = null,
+    private val overviewComponent: (AppComponentContext) -> OverviewComponent,
+) : ConfigsetsComponent,
+    AppComponentContext by componentContext {
+
+    private val navigation = StackNavigation<Configuration>()
+    private val stack = childStack(
+        source = navigation,
+        serializer = Configuration.serializer(),
+        initialStack = { calculateInitialStack(destination) },
+        handleBackButton = true,
+        childFactory = ::createChild,
+    )
+
+    override val childStack: Value<ChildStack<*, Child>> = stack
+    constructor(
+        componentContext: AppComponentContext,
+        storeFactory: StoreFactory,
+        httpClient: HttpClient,
+    ) : this (
+        componentContext = componentContext,
+        storeFactory = storeFactory,
+        httpClient = httpClient,
+        destination = null,
+        overviewComponent = { childContext ->
+            DefaultOverviewComponent(
+                componentContext = childContext,
+                storeFactory = storeFactory,
+                httpClient = httpClient,
+            )
+        },
+    )
+
+    @Serializable
+    private sealed interface Configuration {
+        @Serializable
+        data object Overview : Configuration
+    }
+
+    private fun createChild(
+        configuration: Configuration,
+        componentContext: AppComponentContext,
+    ): Child = when (configuration) {
+        Configuration.Overview -> Child.Overview(overviewComponent(componentContext))
+    }
+
+    /**
+     * Calculates the initial stack based on the destination provided.
+     */
+    private fun calculateInitialStack(destination: String?): List<Configuration> = listOf(
+        when (destination) {
+            "Overview" -> Configuration.Overview
+            else -> Configuration.Overview
+        },
+    )
+
+    private val mainScope = coroutineScope(SupervisorJob() + mainContext)
+    private val ioScope = coroutineScope(SupervisorJob() + ioContext)
+
+    private val store = instanceKeeper.getStore {
+        ConfigsetsStoreProvider(
+            storeFactory = storeFactory,
+            client = HttpEnvironmentStoreClient(httpClient),
+            mainContext = mainScope.coroutineContext,
+            ioContext = ioScope.coroutineContext,
+        ).provide()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    override val model = store.stateFlow.map(mainScope, configsetsStateToModel)
+
+    override fun onSelectTab(tab: ConfigsetsTab) {
+        store.accept(ConfigsetsStore.Intent.SelectTab(tab))

Review Comment:
   Feel free to add `import 
   import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent` to reduce the chained words referencing or using Intents of the component's store.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsConponent.kt:
##########
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets.integration
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.mvikotlin.core.instancekeeper.getStore
+import com.arkivanov.mvikotlin.core.store.StoreFactory
+import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.serialization.Serializable
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.components.configsets.overview.integration.DefaultOverviewComponent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
+import org.apache.solr.ui.utils.AppComponentContext
+import org.apache.solr.ui.utils.coroutineScope
+import org.apache.solr.ui.utils.map
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Default implementation of the [ConfigsetsComponent].
+ */
+class DefaultConfigsetsConponent internal constructor(
+    componentContext: AppComponentContext,
+    storeFactory: StoreFactory,
+    httpClient: HttpClient,
+    destination: String? = null,

Review Comment:
   I would go with an enum here, so that we limit the possible destinations to only known values. A string can be anything, but an enum cannot.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStore.kt:
##########
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.components.configsets.store
+
+import com.arkivanov.mvikotlin.core.store.Store
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.State
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+internal interface ConfigsetsStore : Store<Intent, State, Nothing> {
+    sealed interface Intent {
+        /**
+         * Intent for selecting tab.
+         */
+        data class SelectTab(val tab: ConfigsetsTab) : Intent
+
+        /**
+         * Intent for fetching configsets from the solr.
+         */
+        data object FetchConfigSets : Intent
+
+        data class SelectConfigSet(val configSetName: String) : Intent
+    }
+    data class State(
+        val selectedTab: ConfigsetsTab = ConfigsetsTab.Overview,
+        val selectedConfigset: String = "",
+        val configSets: ListConfigsets = ListConfigsets(),

Review Comment:
   You are exposing here an API model too far into the app. It would be better to use an internal domain model instead, like `List<ConfigSet>` where Configset may be a data model with just a `name` property for now (later we may consider using IDs, display names or just names, and other values).
   
   Because we are free to use whatever models we want in the frontend, we don't have to keep the API structure across the app, and it will also be easier to migrate later to v2 endpoints (in case v1 is used) if we just map it as soon as we retrieve the data (so at the store client implementation).
   
   Also, using a List directly will allow us later to directly use the field for iterating over it (`configSets.forEach { configset -> ... }`)



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(
+    selectedTab: ConfigsetsTab,
+    selectTab: (ConfigsetsTab) -> Unit,
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: ListConfigsets,
+    modifier: Modifier = Modifier,
+    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
+    },

Review Comment:
   This is now the case where I would ask you "why does a navigation bar that is used for navigation need to know configsets? And why does it have a content? What can I put into it, and where is it added?"
   
   By definition, I would expect the navigation bar to be only the elements on the top that I can choose from, so the navigation items. You are now providing the content as well, which kinda confuses the user of this composable.
   
   At the same time, I am wondering, does the navigation bar need to know about the configsets? I am just navigating through tabs, am I not?



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(
+    selectedTab: ConfigsetsTab,
+    selectTab: (ConfigsetsTab) -> Unit,
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: ListConfigsets,
+    modifier: Modifier = Modifier,
+    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
+    },
+) {
+    if (availableConfigsets.names.isEmpty()) {
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            Text("No configsets available")
+        }
+        return
+    }
+
+    var expanded by remember { mutableStateOf(false) }
+    val selectedIndex = ConfigsetsTab.entries.indexOf(selectedTab)
+    Column(modifier) {
+        ScrollableTabRow(
+            selectedTabIndex = selectedIndex,
+            edgePadding = 16.dp,
+            divider = { HorizontalDivider(thickness = 1.dp) },
+            indicator = { pos ->
+                TabRowDefaults.SecondaryIndicator(
+                    Modifier.tabIndicatorOffset(pos[selectedIndex]),
+                )
+            },
+        ) {
+            ConfigsetsTab.entries.forEach { tab ->
+                Tab(
+                    selected = selectedTab == tab,
+                    onClick = { selectTab(tab) },
+                    text = { Text(stringResource(tabLabelRes(tab)), maxLines = 1, overflow = TextOverflow.Ellipsis) },
+                )
+            }
+        }
+
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            ExposedDropdownMenuBox(
+                expanded = expanded,
+                onExpandedChange = { expanded = !expanded },
+                modifier = Modifier.widthIn(min = 256.dp).weight(1f),
+            ) {
+                OutlinedTextField(
+                    value = selectedConfigSet,
+                    onValueChange = {},
+                    readOnly = true,
+                    label = { Text("Configset") },
+                    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+                    modifier = Modifier.menuAnchor().fillMaxWidth(),
+                )
+                ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+                    availableConfigsets.names.forEach { name ->
+                        DropdownMenuItem(
+                            text = { Text(name) },
+                            onClick = {
+                                selectConfigset(name)
+                                expanded = false
+                            },
+                        )
+                    }
+                }
+            }
+        }

Review Comment:
   This block seems like it does not belong here 🤔 



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/ListConfigsets.kt:
##########
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.components.configsets.data
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * List of config sets.
+ */
+@Serializable
+data class ListConfigsets(
+    @SerialName("configSets")
+    val names: List<String> = emptyList(),

Review Comment:
   Is there a special reason you chose to explicitly define a serial name, rather than using `val configSets`?



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.Value
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Contract for the Configsets feature.
+ *
+ * Responsibilities:
+ * - Owns tab/navigation state within the Configsets screen.
+ * - Loads and exposes the list of available configsets.
+ * - Persists the currently selected tab and configset.

Review Comment:
   Point 1 and 3 is the same I believe? Point 2 is responsibility eventually out of scope (not necessarily though).



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/HttpEnvironmentStoreClient.kt:
##########
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets.integration
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.http.isSuccess
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
+
+/**
+ * Client implementation of the [ConfigsetsStoreProvider.Client] that makes use
+ * of a preconfigured HTTP client for accessing the Solr API.
+ *
+ * @property httpClient HTTP client to use for accessing the API. The client has to be
+ * configured with a default request that includes the host, port and schema. The client
+ * should also include the necessary authentication data if authentication / authorization
+ * is enabled.
+ */
+class HttpEnvironmentStoreClient(

Review Comment:
   I believe you meant here `HttpConfigStoreClient`?



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStore.kt:
##########
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.components.configsets.store
+
+import com.arkivanov.mvikotlin.core.store.Store
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.State
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+internal interface ConfigsetsStore : Store<Intent, State, Nothing> {
+    sealed interface Intent {
+        /**
+         * Intent for selecting tab.
+         */
+        data class SelectTab(val tab: ConfigsetsTab) : Intent
+
+        /**
+         * Intent for fetching configsets from the solr.
+         */
+        data object FetchConfigSets : Intent

Review Comment:
   Can the user manually fetch the configsets? Because intents describe a user's intent / wish to do something (on request). If not, consider using only `Action. FetchInitialConfigsets` in `ConfigsetsStoreProvider` (for initial fetch) and removing this.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContent.kt:
##########
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import ConfigsetsNavBarComponent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.arkivanov.decompose.extensions.compose.subscribeAsState
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun ConfigsetsContent(
+    component: ConfigsetsComponent,
+    modifier: Modifier = Modifier,
+) = FlowRow(
+    modifier = modifier,
+    horizontalArrangement = Arrangement.spacedBy(16.dp),
+    verticalArrangement = Arrangement.spacedBy(16.dp),
+) {
+    val model by component.model.collectAsState()
+
+    // Decompose child to access OverviewComponent
+    val stack by component.childStack.subscribeAsState()
+    val currentChild = stack.active.instance
+
+    ConfigsetsNavBarComponent(
+        selectedTab = model.selectedTab,
+        selectTab = { tab: ConfigsetsTab -> component.onSelectTab(tab) },
+        selectedConfigSet = model.selectedConfigset,
+        selectConfigset = { s: String -> component.onSelectConfigset(s) },
+        availableConfigsets = model.configSets,
+        content = { tab, _ ->
+            when (tab) {
+                ConfigsetsTab.Overview -> {
+                    when (val child = currentChild) {
+                        is Child.Overview ->
+                            OverviewContent(component = child.component)
+                        else ->
+                            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

Review Comment:
   Always go with or without names for the parameters, avoid mixing it. Consider also writing params in new lines if more than 2.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStoreProvider.kt:
##########
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets.store
+
+import com.arkivanov.mvikotlin.core.store.Reducer
+import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
+import com.arkivanov.mvikotlin.core.store.Store
+import com.arkivanov.mvikotlin.core.store.StoreFactory
+import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.State
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Store provider that [provide]s instances of [ConfigsetsStore].
+ *
+ * @property storeFactory Store factory to use for creating the store.
+ * @property client Client implementation to use for resolving [Intent]s and [Action]s.
+ * @property ioContext Coroutine context used for IO activity.
+ */
+internal class ConfigsetsStoreProvider(
+    private val storeFactory: StoreFactory,
+    private val client: Client,
+    private val mainContext: CoroutineContext,
+    private val ioContext: CoroutineContext,
+) {
+
+    fun provide(): ConfigsetsStore = object :
+        ConfigsetsStore,
+        Store<Intent, State, Nothing> by storeFactory.create(
+            name = "ConfigsetsStore",
+            initialState = State(),
+            bootstrapper = SimpleBootstrapper(Action.FetchInitialConfigsets),
+            executorFactory = ::ExecutorImpl,
+            reducer = ReducerImpl,
+        ) {}
+
+    private sealed interface Action {
+        /**
+         * Action used for initiating the initial fetch of configsets data.
+         */
+        data object FetchInitialConfigsets : Action
+    }
+
+    private sealed interface Message {
+        data class ConfigSetsUpdated(val configsets: ListConfigsets) : Message
+        data class SelectedTabChanged(val tab: ConfigsetsTab) : Message
+        data class SelectedConfigSetChanged(val configsetName: String) : Message
+    }
+
+    private inner class ExecutorImpl : CoroutineExecutor<Intent, Action, State, Message, Nothing>(mainContext) {
+        override fun executeAction(action: Action) = when (action) {
+            Action.FetchInitialConfigsets -> {
+                fetchConfigSets()
+            }
+        }
+        override fun executeIntent(intent: Intent) = when (intent) {
+            is Intent.FetchConfigSets -> fetchConfigSets()
+            is Intent.SelectTab -> dispatch(Message.SelectedTabChanged(intent.tab))
+            is Intent.SelectConfigSet -> dispatch(Message.SelectedConfigSetChanged(intent.configSetName))
+        }
+        private fun fetchConfigSets() {
+            scope.launch {
+                withContext(ioContext) {
+                    client.fetchConfigSets()
+                }.onSuccess { sets ->
+                    dispatch(Message.ConfigSetsUpdated(sets))
+                }
+            }
+        }
+    }
+
+    /**
+     * 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.ConfigSetsUpdated -> copy(configSets = msg.configsets)
+            is Message.SelectedTabChanged -> copy(selectedTab = msg.tab)
+            is Message.SelectedConfigSetChanged -> copy(selectedConfigset = msg.configsetName)
+        }
+    }
+
+    /**
+     * Client interface for fetching configsets information.
+     */
+    interface Client {
+        /** To fetch a list of configsets. */
+        suspend fun fetchConfigSets(): Result<ListConfigsets>

Review Comment:
   So I would recommend not to return a `ListConfigsets` (API model), but rather a `List<Configset>` (domain model) here.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.Value
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Contract for the Configsets feature.
+ *
+ * Responsibilities:
+ * - Owns tab/navigation state within the Configsets screen.
+ * - Loads and exposes the list of available configsets.
+ * - Persists the currently selected tab and configset.
+ *
+ * Exposed state:
+ * - [model]: reactive UI model (configset names, selected tab, selected configset).
+ *
+ * Actions:
+ * - [onSelectTab]: update the active tab.
+ * - [onSelectConfigset]: change the active configset.
+ *
+ * Notes:
+ * - Scope: feature-local only; app-level navigation is owned by [MainComponent].
+ * - State restoration is implementation-defined (e.g., MVIKotlin Store / Decompose StateKeeper).
+ *
+ * See also:
+ * - Decompose (component lifecycle & composition)
+ * - MVIKotlin (unidirectional data flow)
+ */
+interface ConfigsetsComponent {
+
+    /**
+     * Child stack that holds the navigation state.
+     */
+    val childStack: Value<ChildStack<*, Child>>
+
+    /**
+     * All possible navigation targets (children) within the Configsets feature.
+     *
+     * Each child wraps its own component, which holds the state and logic for that screen.
+     */
+    sealed interface Child {
+        data class Overview(val component: OverviewComponent) : Child
+    }
+
+    /**
+     * UI model for the Configsets screen.
+     *
+     * @property selectedTab Active tab.
+     * @property configSets List of available configsets (domain model).
+     * @property selectedConfigset Name of the active configset (non-null when available).

Review Comment:
   Is it perhaps better to allow null values, so that
   - `null` means no selection (no reference, no configset)
   - `string` means selection (actual configset exists)
   
   Of course an empty string is also not a valid configset, but from reading the field I believe `selectedConfigset: String? = null` would be easier to handle.
   
   Since in Kotlin you have explicit nullability, you can handle it differently like with `value?.let { ... }` (a Kotlin feature useful for executing code in a scoped block only if a value is not null).



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(

Review Comment:
   Since it is a plain composable function, avoid using the kinda "reserved" suffix "Component". This avoids confusion with the component interfaces and implementations. Just name it `ConfigsetsNavBar` for example.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsConponent.kt:
##########
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets.integration
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.mvikotlin.core.instancekeeper.getStore
+import com.arkivanov.mvikotlin.core.store.StoreFactory
+import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.serialization.Serializable
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.components.configsets.overview.integration.DefaultOverviewComponent
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStore
+import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
+import org.apache.solr.ui.utils.AppComponentContext
+import org.apache.solr.ui.utils.coroutineScope
+import org.apache.solr.ui.utils.map
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Default implementation of the [ConfigsetsComponent].
+ */
+class DefaultConfigsetsConponent internal constructor(
+    componentContext: AppComponentContext,
+    storeFactory: StoreFactory,
+    httpClient: HttpClient,
+    destination: String? = null,
+    private val overviewComponent: (AppComponentContext) -> OverviewComponent,
+) : ConfigsetsComponent,
+    AppComponentContext by componentContext {
+
+    private val navigation = StackNavigation<Configuration>()
+    private val stack = childStack(
+        source = navigation,
+        serializer = Configuration.serializer(),
+        initialStack = { calculateInitialStack(destination) },
+        handleBackButton = true,
+        childFactory = ::createChild,
+    )
+
+    override val childStack: Value<ChildStack<*, Child>> = stack
+    constructor(
+        componentContext: AppComponentContext,
+        storeFactory: StoreFactory,
+        httpClient: HttpClient,
+    ) : this (
+        componentContext = componentContext,
+        storeFactory = storeFactory,
+        httpClient = httpClient,
+        destination = null,
+        overviewComponent = { childContext ->
+            DefaultOverviewComponent(
+                componentContext = childContext,
+                storeFactory = storeFactory,
+                httpClient = httpClient,
+            )
+        },
+    )
+
+    @Serializable
+    private sealed interface Configuration {
+        @Serializable
+        data object Overview : Configuration
+    }
+
+    private fun createChild(
+        configuration: Configuration,
+        componentContext: AppComponentContext,
+    ): Child = when (configuration) {
+        Configuration.Overview -> Child.Overview(overviewComponent(componentContext))
+    }
+
+    /**
+     * Calculates the initial stack based on the destination provided.
+     */
+    private fun calculateInitialStack(destination: String?): List<Configuration> = listOf(
+        when (destination) {
+            "Overview" -> Configuration.Overview

Review Comment:
   Consider enums (limited possibilities) instead of strings, this way, no else case is needed (thanks to type-safety).



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(
+    selectedTab: ConfigsetsTab,
+    selectTab: (ConfigsetsTab) -> Unit,
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: ListConfigsets,
+    modifier: Modifier = Modifier,
+    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
+    },
+) {
+    if (availableConfigsets.names.isEmpty()) {
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            Text("No configsets available")
+        }
+        return
+    }
+
+    var expanded by remember { mutableStateOf(false) }

Review Comment:
   Another UI state that should be added to a component's state. If you do not add it, you won't be able later to load additional information on demand once the user expands it. 🤔 



##########
solr/ui/src/commonMain/composeResources/values/strings.xml:
##########
@@ -50,6 +50,16 @@
   <string name="nav_security">Security</string>
   <string name="nav_thread_dump">Thread Dump</string>
 
+  <!-- Configsets (nav) -->
+  <string name="configsets_overview">Overview</string>
+  <string name="configsets_libraries">Libraries</string>
+  <string name="configsets_files">Files</string>
+  <string name="configsets_schema">Schema</string>
+  <string name="configsets_update_configuration">Update Configuration</string>
+  <string name="configsets_index_query">Index / Query</string>
+  <string name="configsets_request_handlers">Request Handlers / Dispatchers</string>
+  <string name="configsets_search_components">Search Components</string>

Review Comment:
   I believe that we could use general terms here, without prefixing it with `configsets`, because we will likely have similar words (like "overview", "files", "schema") in other places too, so reusing them would be beneficial (smaller files, less duplication).



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.ui.components.configsets
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.Value
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.overview.OverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+/**
+ * Contract for the Configsets feature.
+ *
+ * Responsibilities:
+ * - Owns tab/navigation state within the Configsets screen.
+ * - Loads and exposes the list of available configsets.
+ * - Persists the currently selected tab and configset.
+ *
+ * Exposed state:
+ * - [model]: reactive UI model (configset names, selected tab, selected configset).
+ *
+ * Actions:
+ * - [onSelectTab]: update the active tab.
+ * - [onSelectConfigset]: change the active configset.
+ *
+ * Notes:
+ * - Scope: feature-local only; app-level navigation is owned by [MainComponent].
+ * - State restoration is implementation-defined (e.g., MVIKotlin Store / Decompose StateKeeper).
+ *
+ * See also:
+ * - Decompose (component lifecycle & composition)
+ * - MVIKotlin (unidirectional data flow)

Review Comment:
   No need to repeat yourself here, because readers can look into the interface. When updating model or actions, you would also have to update documentation here (can be avoided). Nor do you have to provide information that is "common" knowledge or out of scope of this configset ("see also" points). You can reduce it to only what this component is all about, "managing the navigation and content / layout of the configset screen". 



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/overview/OverviewComponent.kt:
##########
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.components.configsets.overview
+
+interface OverviewComponent

Review Comment:
   I would go with `ConfigsetOverviewComponent` to avoid naming conflicts later on when other sub-sections have an "overview" component.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "iamsanjay (via GitHub)" <gi...@apache.org>.
iamsanjay commented on PR #3521:
URL: https://github.com/apache/solr/pull/3521#issuecomment-3245997410

   I believe even ConfigsetsNavBar can be a Solr component because that is something that we will have for other screens as well.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #3521:
URL: https://github.com/apache/solr/pull/3521#issuecomment-3707470311

   This PR is now closed due to 60 days of inactivity after being marked as stale.  Re-opening this PR is still possible, in which case it will be marked as active again.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "iamsanjay (via GitHub)" <gi...@apache.org>.
iamsanjay commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2307935931


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBarComponent.kt:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.generated.resources.Res
+import org.apache.solr.ui.generated.resources.configsets_files
+import org.apache.solr.ui.generated.resources.configsets_index_query
+import org.apache.solr.ui.generated.resources.configsets_overview
+import org.apache.solr.ui.generated.resources.configsets_request_handlers
+import org.apache.solr.ui.generated.resources.configsets_schema
+import org.apache.solr.ui.generated.resources.configsets_search_components
+import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun ConfigsetsNavBarComponent(
+    selectedTab: ConfigsetsTab,
+    selectTab: (ConfigsetsTab) -> Unit,
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: ListConfigsets,
+    modifier: Modifier = Modifier,
+    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { tab, _ ->
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(tabLabelRes(tab))) }
+    },

Review Comment:
   Lets move the configset dropdown and content outside of this component to ConfigsetsComponent



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] closed pull request #3521: SOLR-17847: Introduce Navigation to Configsets
URL: https://github.com/apache/solr/pull/3521


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh commented on PR #3521:
URL: https://github.com/apache/solr/pull/3521#issuecomment-3236369011

   In another PR it was noted that we may be missing the appropriate api endpoints for config sets!   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #3521:
URL: https://github.com/apache/solr/pull/3521#issuecomment-3488511510

   This PR has had no activity for 60 days and is now labeled as stale.  Any new activity will remove the stale label.  To attract more reviewers, please tag people who might be familiar with the code area and/or notify the dev@solr.apache.org mailing list. To exempt this PR from being marked as stale, make it a draft PR or add the label "exempt-stale". If left unattended, this PR will be closed after another 60 days of inactivity. Thank you for your contribution!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "iamsanjay (via GitHub)" <gi...@apache.org>.
iamsanjay commented on PR #3521:
URL: https://github.com/apache/solr/pull/3521#issuecomment-3236280843

   We   have two ways of looking at the config
   
   - the Configsets API (list/upload/delete, names only) and
   - the collection-scoped /solr/{collection}/config endpoints (actual config: handlers, params, etc.).
   
   The solrconfig details on the first screen aren’t exposed by the Configsets API; unless we fetch and parse files ourselves, we have to go via a collection. That suggests we’re missing the collection context—in practice, or my personal experience so far, edits happen from the collection side, not from the configset context.  And the whole thought came to my mind when we discussed Configset object, because configsets api cannot share much info about config unless we go via collection scoped endpoints.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "iamsanjay (via GitHub)" <gi...@apache.org>.
iamsanjay commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2324706156


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = 16.dp, vertical = 8.dp),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        ExposedDropdownMenuBox(
+            expanded = expanded,
+            onExpandedChange = { expanded = !expanded },
+            modifier = Modifier.widthIn(min = 256.dp).weight(1f),
+        ) {
+            OutlinedTextField(
+                value = selectedConfigSet,
+                onValueChange = {},
+                readOnly = true,
+                label = { Text("Configset") },

Review Comment:
   I believe it is. You may have reviewed the older commit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


Re: [PR] SOLR-17847: Introduce Navigation to Configsets [solr]

Posted by "iamsanjay (via GitHub)" <gi...@apache.org>.
iamsanjay commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2324701383


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }

Review Comment:
   Yes this one is already fixed. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org