@@ -2,42 +2,279 @@ package cc.unitmesh.devti.mcp.client
2
2
3
3
import cc.unitmesh.devti.AutoDevBundle
4
4
import cc.unitmesh.devti.util.AutoDevCoroutineScope
5
+ import com.intellij.icons.AllIcons
5
6
import com.intellij.openapi.diagnostic.logger
6
7
import com.intellij.openapi.project.Project
7
8
import com.intellij.openapi.ui.DialogWrapper
9
+ import com.intellij.ui.JBColor
8
10
import com.intellij.ui.components.JBLoadingPanel
9
11
import com.intellij.ui.components.JBScrollPane
12
+ import com.intellij.ui.components.JBTextField
10
13
import com.intellij.ui.table.JBTable
14
+ import com.intellij.util.ui.JBUI
15
+ import com.intellij.util.ui.components.BorderLayoutPanel
11
16
import io.modelcontextprotocol.kotlin.sdk.Tool
12
17
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
15
23
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
17
27
import javax.swing.table.DefaultTableModel
28
+ import javax.swing.table.TableRowSorter
18
29
19
30
class McpServicesTestDialog (private val project : Project ) : DialogWrapper(project) {
20
31
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" ))
22
33
private val table = JBTable (tableModel)
23
34
private val job = SupervisorJob ()
24
35
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
+ }
25
81
26
82
init {
27
83
title = AutoDevBundle .message(" sketch.mcp.testMcp" )
28
- init ()
29
-
30
84
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 ()
31
93
loadServices()
32
94
}
33
95
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
+
34
262
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
+
35
269
val scrollPane = JBScrollPane (table)
36
270
scrollPane.preferredSize = Dimension (800 , 400 )
37
271
272
+ loadingPanel.add(searchPanel, BorderLayout .NORTH )
38
273
loadingPanel.add(scrollPane, BorderLayout .CENTER )
39
274
40
- return loadingPanel
275
+ mainPanel.add(loadingPanel, BorderLayout .CENTER )
276
+
277
+ return mainPanel
41
278
}
42
279
43
280
override fun getPreferredSize (): Dimension {
@@ -52,13 +289,25 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
52
289
AutoDevCoroutineScope .workerScope(project).launch {
53
290
try {
54
291
val serverManager = CustomMcpServerManager .instance(project)
55
- val serverInfos = serverManager.collectServerInfos()
292
+ val serverInfos: Map < String , List < Tool >> = serverManager.collectServerInfos()
56
293
57
294
updateTable(serverInfos)
295
+
296
+ // Expand all servers by default
297
+ serverInfos.keys.forEach { server ->
298
+ tableModel.expandedGroups.add(server)
299
+ }
300
+ updateRowFilter()
301
+
58
302
loadingPanel.stopLoading()
59
303
isLoading.set(false )
60
304
} 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
+
62
311
loadingPanel.stopLoading()
63
312
isLoading.set(false )
64
313
@@ -69,13 +318,18 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
69
318
70
319
private fun updateTable (serverInfos : Map <String , List <Tool >>) {
71
320
tableModel.rowCount = 0
321
+ tableModel.groupRows.clear()
72
322
73
323
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" )
75
326
return
76
327
}
77
328
78
329
serverInfos.forEach { (server, tools) ->
330
+ // Add server group row
331
+ val serverRowIndex = tableModel.addGroupRow(server)
332
+
79
333
if (tools.isEmpty()) {
80
334
tableModel.addRow(arrayOf(server, " No tools found" , " " ))
81
335
} else {
@@ -90,4 +344,4 @@ class McpServicesTestDialog(private val project: Project) : DialogWrapper(projec
90
344
job.cancel()
91
345
super .dispose()
92
346
}
93
- }
347
+ }
0 commit comments