Skip to content

Commit 4498bd9

Browse files
committed
feat(devti): implement server grouping and search in McpServicesTestDialog- Add server grouping functionality to the table
- Implement search feature for servers and tools - Improve table rendering and styling - Refactor table model to support grouping - Optimize row filtering for search
1 parent a51dcc2 commit 4498bd9

File tree

1 file changed

+265
-11
lines changed

1 file changed

+265
-11
lines changed

core/src/main/kotlin/cc/unitmesh/devti/mcp/client/McpServicesTestDialog.kt

Lines changed: 265 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,279 @@ package cc.unitmesh.devti.mcp.client
22

33
import cc.unitmesh.devti.AutoDevBundle
44
import cc.unitmesh.devti.util.AutoDevCoroutineScope
5+
import com.intellij.icons.AllIcons
56
import com.intellij.openapi.diagnostic.logger
67
import com.intellij.openapi.project.Project
78
import com.intellij.openapi.ui.DialogWrapper
9+
import com.intellij.ui.JBColor
810
import com.intellij.ui.components.JBLoadingPanel
911
import com.intellij.ui.components.JBScrollPane
12+
import com.intellij.ui.components.JBTextField
1013
import com.intellij.ui.table.JBTable
14+
import com.intellij.util.ui.JBUI
15+
import com.intellij.util.ui.components.BorderLayoutPanel
1116
import io.modelcontextprotocol.kotlin.sdk.Tool
1217
import kotlinx.coroutines.*
13-
import java.awt.BorderLayout
14-
import java.awt.Dimension
18+
import java.awt.*
19+
import java.awt.event.KeyAdapter
20+
import java.awt.event.KeyEvent
21+
import java.awt.event.MouseAdapter
22+
import java.awt.event.MouseEvent
1523
import java.util.concurrent.atomic.AtomicBoolean
16-
import javax.swing.JComponent
24+
import javax.swing.*
25+
import javax.swing.border.EmptyBorder
26+
import javax.swing.table.DefaultTableCellRenderer
1727
import javax.swing.table.DefaultTableModel
28+
import javax.swing.table.TableRowSorter
1829

1930
class McpServicesTestDialog(private val project: Project) : DialogWrapper(project) {
2031
private val loadingPanel = JBLoadingPanel(BorderLayout(), this.disposable)
21-
private val tableModel = DefaultTableModel(arrayOf("Server", "Tool Name", "Description"), 0)
32+
private val tableModel = GroupableTableModel(arrayOf("Server", "Tool Name", "Description"))
2233
private val table = JBTable(tableModel)
2334
private val job = SupervisorJob()
2435
private val isLoading = AtomicBoolean(false)
36+
private val searchField = JBTextField()
37+
private val rowSorter = TableRowSorter<GroupableTableModel>(tableModel)
38+
39+
// Custom table model that supports grouping
40+
class GroupableTableModel(columnNames: Array<String>) : DefaultTableModel(columnNames, 0) {
41+
val groupRows = mutableMapOf<Int, String>() // Maps row index to group name
42+
val expandedGroups = mutableSetOf<String>() // Set of expanded group names
43+
44+
fun addGroupRow(groupName: String): Int {
45+
val rowIndex = rowCount
46+
addRow(arrayOf(groupName, "", ""))
47+
groupRows[rowIndex] = groupName
48+
return rowIndex
49+
}
50+
51+
fun isGroupRow(row: Int): Boolean {
52+
return groupRows.containsKey(row)
53+
}
54+
55+
fun getGroupForRow(row: Int): String? {
56+
if (isGroupRow(row)) {
57+
return groupRows[row]
58+
}
59+
60+
for (i in row downTo 0) {
61+
if (isGroupRow(i)) {
62+
return groupRows[i]
63+
}
64+
}
65+
66+
return null
67+
}
68+
69+
fun toggleGroupExpansion(groupName: String) {
70+
if (expandedGroups.contains(groupName)) {
71+
expandedGroups.remove(groupName)
72+
} else {
73+
expandedGroups.add(groupName)
74+
}
75+
}
76+
77+
fun isGroupExpanded(groupName: String): Boolean {
78+
return expandedGroups.contains(groupName)
79+
}
80+
}
2581

2682
init {
2783
title = AutoDevBundle.message("sketch.mcp.testMcp")
28-
init()
29-
3084
table.preferredScrollableViewportSize = Dimension(800, 400)
85+
table.rowSorter = rowSorter
86+
table.rowHeight = 30
87+
table.setShowGrid(false)
88+
table.intercellSpacing = Dimension(0, 0)
89+
90+
setupTableRenderers()
91+
setupSearch()
92+
init()
3193
loadServices()
3294
}
3395

96+
private fun setupTableRenderers() {
97+
val serverRenderer = object : DefaultTableCellRenderer() {
98+
override fun getTableCellRendererComponent(
99+
table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int
100+
): Component {
101+
val panel = BorderLayoutPanel()
102+
panel.background = if (isSelected) table.selectionBackground else table.background
103+
104+
val modelRow = table.convertRowIndexToModel(row)
105+
val isGroupRow = (tableModel as GroupableTableModel).isGroupRow(modelRow)
106+
107+
if (isGroupRow && column == 0) {
108+
val groupName = value.toString()
109+
val isExpanded = tableModel.isGroupExpanded(groupName)
110+
111+
val icon = if (isExpanded) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
112+
val iconLabel = JLabel(icon)
113+
iconLabel.border = JBUI.Borders.empty(0, 4, 0, 8)
114+
115+
val textLabel = JLabel(groupName)
116+
textLabel.font = textLabel.font.deriveFont(Font.BOLD)
117+
118+
panel.add(iconLabel, BorderLayout.WEST)
119+
panel.add(textLabel, BorderLayout.CENTER)
120+
121+
val toolCount = getToolCountForServer(groupName)
122+
if (toolCount > 0) {
123+
val countLabel = JLabel("($toolCount)")
124+
countLabel.foreground = JBColor.GRAY
125+
countLabel.border = JBUI.Borders.emptyLeft(8)
126+
panel.add(countLabel, BorderLayout.EAST)
127+
}
128+
129+
panel.border = JBUI.Borders.empty(4, 8, 4, 0)
130+
panel.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
131+
132+
if (isSelected) {
133+
panel.background = table.selectionBackground.brighter()
134+
} else {
135+
panel.background = JBColor.background().brighter()
136+
}
137+
} else {
138+
val label = JLabel(value?.toString() ?: "")
139+
140+
if (column == 0) {
141+
label.border = JBUI.Borders.empty(4, 32, 4, 0)
142+
} else {
143+
label.border = JBUI.Borders.empty(4, 8, 4, 0)
144+
}
145+
146+
panel.add(label, BorderLayout.CENTER)
147+
}
148+
149+
return panel
150+
}
151+
}
152+
153+
val toolRenderer = object : DefaultTableCellRenderer() {
154+
override fun getTableCellRendererComponent(
155+
table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int
156+
): Component {
157+
val component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
158+
border = JBUI.Borders.empty(4, 8, 4, 0)
159+
return component
160+
}
161+
}
162+
163+
val descriptionRenderer = object : DefaultTableCellRenderer() {
164+
override fun getTableCellRendererComponent(
165+
table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int
166+
): Component {
167+
val component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
168+
border = JBUI.Borders.empty(4, 8)
169+
return component
170+
}
171+
}
172+
173+
table.getColumnModel().getColumn(0).cellRenderer = serverRenderer
174+
table.getColumnModel().getColumn(1).cellRenderer = toolRenderer
175+
table.getColumnModel().getColumn(2).cellRenderer = descriptionRenderer
176+
177+
table.addMouseListener(object : MouseAdapter() {
178+
override fun mouseClicked(e: MouseEvent) {
179+
val row = table.rowAtPoint(e.point)
180+
if (row >= 0) {
181+
val modelRow = table.convertRowIndexToModel(row)
182+
if (tableModel.isGroupRow(modelRow)) {
183+
val groupName = tableModel.getValueAt(modelRow, 0) as String
184+
tableModel.toggleGroupExpansion(groupName)
185+
updateRowFilter()
186+
table.repaint()
187+
}
188+
}
189+
}
190+
})
191+
}
192+
193+
private fun getToolCountForServer(server: String): Int {
194+
var count = 0
195+
for (i in 0 until tableModel.rowCount) {
196+
if (!tableModel.isGroupRow(i) &&
197+
tableModel.getValueAt(i, 0) == server &&
198+
tableModel.getValueAt(i, 1) != "No tools found") {
199+
count++
200+
}
201+
}
202+
return count
203+
}
204+
205+
private fun setupSearch() {
206+
searchField.border = JBUI.Borders.empty(8)
207+
searchField.emptyText.text = "Search servers or tools..."
208+
209+
searchField.addKeyListener(object : KeyAdapter() {
210+
override fun keyReleased(e: KeyEvent) {
211+
updateRowFilter()
212+
}
213+
})
214+
}
215+
216+
private fun updateRowFilter() {
217+
val searchText = searchField.text.lowercase()
218+
219+
rowSorter.rowFilter = object : RowFilter<GroupableTableModel, Int>() {
220+
override fun include(entry: RowFilter.Entry<out GroupableTableModel, out Int>): Boolean {
221+
val modelRow = entry.identifier as Int
222+
val model = entry.model as GroupableTableModel
223+
224+
if (model.isGroupRow(modelRow)) {
225+
if (searchText.isNotEmpty()) {
226+
val groupName = model.getValueAt(modelRow, 0) as String
227+
228+
for (i in 0 until model.rowCount) {
229+
if (!model.isGroupRow(i) && model.getGroupForRow(i) == groupName) {
230+
val server = model.getValueAt(i, 0) as String
231+
val toolName = model.getValueAt(i, 1) as String
232+
val description = model.getValueAt(i, 2)?.toString() ?: ""
233+
234+
if (server.lowercase().contains(searchText) ||
235+
toolName.lowercase().contains(searchText) ||
236+
description.lowercase().contains(searchText)) {
237+
return true
238+
}
239+
}
240+
}
241+
return false
242+
}
243+
return true
244+
}
245+
246+
val groupName = model.getGroupForRow(modelRow)
247+
if (searchText.isNotEmpty()) {
248+
val server = model.getValueAt(modelRow, 0) as String
249+
val toolName = model.getValueAt(modelRow, 1) as String
250+
val description = model.getValueAt(modelRow, 2)?.toString() ?: ""
251+
252+
return server.lowercase().contains(searchText) ||
253+
toolName.lowercase().contains(searchText) ||
254+
description.lowercase().contains(searchText)
255+
}
256+
257+
return groupName != null && model.isGroupExpanded(groupName)
258+
}
259+
}
260+
}
261+
34262
override fun createCenterPanel(): JComponent {
263+
val mainPanel = JPanel(BorderLayout())
264+
265+
val searchPanel = JPanel(BorderLayout())
266+
searchPanel.border = EmptyBorder(0, 0, 8, 0)
267+
searchPanel.add(searchField, BorderLayout.CENTER)
268+
35269
val scrollPane = JBScrollPane(table)
36270
scrollPane.preferredSize = Dimension(800, 400)
37271

272+
loadingPanel.add(searchPanel, BorderLayout.NORTH)
38273
loadingPanel.add(scrollPane, BorderLayout.CENTER)
39274

40-
return loadingPanel
275+
mainPanel.add(loadingPanel, BorderLayout.CENTER)
276+
277+
return mainPanel
41278
}
42279

43280
override fun getPreferredSize(): Dimension {
@@ -52,13 +289,25 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
52289
AutoDevCoroutineScope.workerScope(project).launch {
53290
try {
54291
val serverManager = CustomMcpServerManager.instance(project)
55-
val serverInfos = serverManager.collectServerInfos()
292+
val serverInfos: Map<String, List<Tool>> = serverManager.collectServerInfos()
56293

57294
updateTable(serverInfos)
295+
296+
// Expand all servers by default
297+
serverInfos.keys.forEach { server ->
298+
tableModel.expandedGroups.add(server)
299+
}
300+
updateRowFilter()
301+
58302
loadingPanel.stopLoading()
59303
isLoading.set(false)
60304
} catch (e: Exception) {
61-
tableModel.addRow(arrayOf("Error", e.message, ""))
305+
tableModel.rowCount = 0
306+
val errorGroupRow = tableModel.addGroupRow("Error")
307+
tableModel.addRow(arrayOf("Error", e.message ?: "Unknown error", ""))
308+
tableModel.expandedGroups.add("Error")
309+
updateRowFilter()
310+
62311
loadingPanel.stopLoading()
63312
isLoading.set(false)
64313

@@ -69,13 +318,18 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
69318

70319
private fun updateTable(serverInfos: Map<String, List<Tool>>) {
71320
tableModel.rowCount = 0
321+
tableModel.groupRows.clear()
72322

73323
if (serverInfos.isEmpty()) {
74-
tableModel.addRow(arrayOf("No servers found", "", ""))
324+
val noServersRow = tableModel.addGroupRow("No servers found")
325+
tableModel.expandedGroups.add("No servers found")
75326
return
76327
}
77328

78329
serverInfos.forEach { (server, tools) ->
330+
// Add server group row
331+
val serverRowIndex = tableModel.addGroupRow(server)
332+
79333
if (tools.isEmpty()) {
80334
tableModel.addRow(arrayOf(server, "No tools found", ""))
81335
} else {
@@ -90,4 +344,4 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
90344
job.cancel()
91345
super.dispose()
92346
}
93-
}
347+
}

0 commit comments

Comments
 (0)