@@ -30,23 +30,29 @@ import io.ktor.server.routing.routing
30
30
import kotlinx.html.BODY
31
31
import kotlinx.html.FlowContent
32
32
import kotlinx.html.UL
33
+ import kotlinx.html.a
33
34
import kotlinx.html.b
34
35
import kotlinx.html.body
35
36
import kotlinx.html.br
36
37
import kotlinx.html.button
38
+ import kotlinx.html.classes
37
39
import kotlinx.html.div
40
+ import kotlinx.html.getForm
38
41
import kotlinx.html.h1
39
42
import kotlinx.html.h3
40
43
import kotlinx.html.id
44
+ import kotlinx.html.label
41
45
import kotlinx.html.li
42
46
import kotlinx.html.link
43
47
import kotlinx.html.script
44
48
import kotlinx.html.small
45
49
import kotlinx.html.stream.appendHTML
46
50
import kotlinx.html.style
51
+ import kotlinx.html.submitInput
47
52
import kotlinx.html.table
48
53
import kotlinx.html.tbody
49
54
import kotlinx.html.td
55
+ import kotlinx.html.textInput
50
56
import kotlinx.html.th
51
57
import kotlinx.html.thead
52
58
import kotlinx.html.title
@@ -61,10 +67,12 @@ import org.modelix.model.api.PNodeAdapter
61
67
import org.modelix.model.api.TreePointer
62
68
import org.modelix.model.client.IModelClient
63
69
import org.modelix.model.lazy.BranchReference
70
+ import org.modelix.model.lazy.CLTree
64
71
import org.modelix.model.lazy.CLVersion
65
72
import org.modelix.model.lazy.RepositoryId
66
73
import org.modelix.model.server.ModelServerPermissionSchema
67
74
import org.modelix.model.server.handlers.IRepositoriesManager
75
+ import org.modelix.model.server.handlers.NodeNotFoundException
68
76
import org.modelix.model.server.templates.PageWithMenuBar
69
77
import kotlin.collections.set
70
78
@@ -109,17 +117,36 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
109
117
return @get
110
118
}
111
119
120
+ // IMPORTANT Do not let `expandTo` be an arbitrary string to avoid code injection.
121
+ // The value of `expandTo` is expanded into JavaScript.
122
+ val expandTo = call.request.queryParameters[" expandTo" ]?.let {
123
+ it.toLongOrNull() ? : return @get call.respondText(" Invalid expandTo value. Provide a node id." , status = HttpStatusCode .BadRequest )
124
+ }
125
+
112
126
repoManager.runWithRepository(repositoryId) {
113
127
val tree = CLVersion .loadFromHash(versionHash, client.storeCache).getTree()
114
128
val rootNode = PNodeAdapter (ITree .ROOT_ID , TreePointer (tree))
115
129
130
+ val expandedNodes = expandTo?.let { nodeId -> getAncestorsAndSelf(nodeId, tree) }.orEmpty()
131
+
116
132
call.respondHtmlTemplate(PageWithMenuBar (" repos/" , " ../../../../.." )) {
117
133
headContent {
118
134
title(" Content Explorer" )
119
135
link(" ../../../../../public/content-explorer.css" , rel = " stylesheet" )
120
136
script(" text/javascript" , src = " ../../../../../public/content-explorer.js" ) {}
137
+ if (expandTo != null ) {
138
+ script(" text/javascript" ) {
139
+ unsafe {
140
+ + """
141
+ document.addEventListener("DOMContentLoaded", function(event) {
142
+ scrollToElement('$expandTo ');
143
+ });
144
+ """ .trimIndent()
145
+ }
146
+ }
147
+ }
121
148
}
122
- bodyContent { contentPageBody(rootNode, versionHash, emptySet() ) }
149
+ bodyContent { contentPageBody(rootNode, versionHash, expandedNodes, expandTo ) }
123
150
}
124
151
}
125
152
}
@@ -188,6 +215,17 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
188
215
}
189
216
}
190
217
218
+ private fun getAncestorsAndSelf (expandTo : Long , tree : CLTree ): Set <String > {
219
+ val seq = generateSequence(expandTo) { id ->
220
+ try {
221
+ tree.resolveElement(id)?.parentId
222
+ } catch (e: org.modelix.model.lazy.NodeNotFoundException ) {
223
+ throw NodeNotFoundException (id, e)
224
+ }
225
+ }
226
+ return seq.map { it.toString() }.toSet()
227
+ }
228
+
191
229
// The method traverses the expanded tree based on the alreadyExpandedNodeIds and
192
230
// collects the expandable (not empty) nodes which are not expanded yet
193
231
private fun collectExpandableChildNodes (under : PNodeAdapter , alreadyExpandedNodeIds : Set <String >): Set <String > {
@@ -206,7 +244,12 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
206
244
return emptySet()
207
245
}
208
246
209
- private fun FlowContent.contentPageBody (rootNode : PNodeAdapter , versionHash : String , expandedNodeIds : Set <String >) {
247
+ private fun FlowContent.contentPageBody (
248
+ rootNode : PNodeAdapter ,
249
+ versionHash : String ,
250
+ expandedNodeIds : Set <String >,
251
+ expandTo : Long? ,
252
+ ) {
210
253
h1 { + " Model Server Content" }
211
254
small {
212
255
style = " color: #888; text-align: center; margin-bottom: 15px;"
@@ -223,24 +266,42 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
223
266
+ " Collapse all"
224
267
}
225
268
}
269
+ getForm(action = " ." ) {
270
+ label {
271
+ htmlFor = " expandTo"
272
+ + " Expand to Node: "
273
+ }
274
+ textInput {
275
+ name = " expandTo"
276
+ placeholder = " nodeId"
277
+ required = true
278
+ }
279
+ submitInput(classes = " btn" ) {
280
+ value = " Go"
281
+ }
282
+ }
226
283
div {
227
284
id = " treeWrapper"
228
285
ul(" treeRoot" ) {
229
- nodeItem(rootNode, expandedNodeIds)
286
+ nodeItem(rootNode, expandedNodeIds, expandTo )
230
287
}
231
288
}
232
289
div {
233
290
id = " nodeInspector"
234
291
}
235
292
}
236
293
237
- private fun UL.nodeItem (node : PNodeAdapter , expandedNodeIds : Set <String >) {
294
+ private fun UL.nodeItem (node : PNodeAdapter , expandedNodeIds : Set <String >, expandTo : Long? = null ) {
238
295
li(" nodeItem" ) {
296
+ id = node.nodeId.toString()
239
297
val expanded = expandedNodeIds.contains(node.nodeId.toString())
240
298
if (node.allChildren.toList().isNotEmpty()) {
241
299
div(if (expanded) " expander expander-expanded" else " expander" ) { unsafe { + " ▶" } }
242
300
}
243
301
div(" nameField" ) {
302
+ if (expandTo == node.nodeId) {
303
+ classes + = " expandedToNameField"
304
+ }
244
305
attributes[" data-nodeid" ] = node.nodeId.toString()
245
306
b {
246
307
val namePropertyUID = BuiltinLanguages .jetbrains_mps_lang_core.INamedConcept .name.getUID()
@@ -268,7 +329,7 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
268
329
if (expanded) {
269
330
ul(" nodeTree" ) {
270
331
for (child in node.allChildren) {
271
- nodeItem(child as PNodeAdapter , expandedNodeIds)
332
+ nodeItem(child as PNodeAdapter , expandedNodeIds, expandTo )
272
333
}
273
334
}
274
335
}
@@ -322,7 +383,14 @@ class ContentExplorer(private val client: IModelClient, private val repoManager:
322
383
tr {
323
384
td { + referenceRole }
324
385
td {
325
- + " ${(node.getReferenceTarget(referenceRole) as ? PNodeAdapter )?.nodeId} "
386
+ val nodeId = (node.getReferenceTarget(referenceRole) as ? PNodeAdapter )?.nodeId
387
+ if (nodeId != null ) {
388
+ a(" ?expandTo=$nodeId " ) {
389
+ + " $nodeId "
390
+ }
391
+ } else {
392
+ + " null"
393
+ }
326
394
}
327
395
td {
328
396
+ " ${node.getReferenceTargetRef(referenceRole)?.serialize()} "
0 commit comments