Skip to content

Commit 384f05d

Browse files
committed
feat(mps-sync-plugin): new action "Open Frontend in Browser"
By default, it opens the history of the current branch. A different URL can be configured using the environment variable `MODELIX_FRONTEND_URL`, which supports the placeholders `{repository}` and `{branch}`.
1 parent d69ab49 commit 384f05d

File tree

9 files changed

+124
-1
lines changed

9 files changed

+124
-1
lines changed

model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.modelix.model.client2
22

3+
import io.ktor.http.Url
34
import org.modelix.kotlin.utils.DeprecationInfo
45
import org.modelix.model.IVersion
56
import org.modelix.model.ObjectDeltaFilter
@@ -102,4 +103,6 @@ interface IModelClientV2 {
102103
suspend fun <R> query(branch: BranchReference, body: (IMonoStep<INode>) -> IMonoStep<R>): R
103104

104105
suspend fun <R> query(repositoryId: RepositoryId, versionHash: String, body: (IMonoStep<INode>) -> IMonoStep<R>): R
106+
107+
fun getFrontendUrl(branch: BranchReference): Url
105108
}

model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.ktor.http.ContentType
2424
import io.ktor.http.HttpHeaders
2525
import io.ktor.http.HttpStatusCode
2626
import io.ktor.http.URLBuilder
27+
import io.ktor.http.Url
2728
import io.ktor.http.appendPathSegments
2829
import io.ktor.http.buildUrl
2930
import io.ktor.http.contentType
@@ -585,6 +586,13 @@ class ModelClientV2(
585586
return ModelQLClient.builder().httpClient(httpClient).url(url.buildString()).build().query(body)
586587
}
587588

589+
override fun getFrontendUrl(branch: BranchReference): Url {
590+
return buildUrl {
591+
takeFrom(baseUrl)
592+
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "frontend")
593+
}
594+
}
595+
588596
override fun close() {
589597
httpClient.close()
590598
}

model-server-openapi/specifications/model-server-v2.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,26 @@ paths:
260260
$ref: '#/components/responses/versionDelta'
261261
default:
262262
$ref: '#/components/responses/GeneralError'
263+
/repositories/{repository}/branches/{branch}/frontend:
264+
get:
265+
tags: [v2]
266+
operationId: redirectToFrontend
267+
parameters:
268+
- name: repository
269+
in: "path"
270+
required: true
271+
schema:
272+
type: string
273+
- name: branch
274+
in: "path"
275+
required: true
276+
schema:
277+
type: string
278+
responses:
279+
"302":
280+
description: "Redirect browser to actual frontend URL"
281+
default:
282+
$ref: '#/components/responses/GeneralError'
263283
/repositories/{repository}/branches/{branch}/hash:
264284
get:
265285
tags:

model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.ktor.server.request.receive
1111
import io.ktor.server.request.receiveChannel
1212
import io.ktor.server.response.respond
1313
import io.ktor.server.response.respondBytesWriter
14+
import io.ktor.server.response.respondRedirect
1415
import io.ktor.server.response.respondText
1516
import io.ktor.server.response.respondTextWriter
1617
import io.ktor.server.routing.RoutingContext
@@ -28,6 +29,7 @@ import org.modelix.authorization.checkPermission
2829
import org.modelix.authorization.getUserName
2930
import org.modelix.authorization.hasPermission
3031
import org.modelix.authorization.requiresLogin
32+
import org.modelix.kotlin.utils.urlEncode
3133
import org.modelix.model.ObjectDeltaFilter
3234
import org.modelix.model.TreeId
3335
import org.modelix.model.api.IBranch
@@ -261,6 +263,15 @@ class ModelReplicationServer(
261263
}
262264
}
263265

266+
override suspend fun RoutingContext.redirectToFrontend(
267+
repository: String,
268+
branch: String,
269+
) {
270+
val urlTemplate = System.getenv("MODELIX_FRONTEND_URL") ?: "../../../../../history/{repository}/{branch}/"
271+
val url = urlTemplate.replace("{repository}", repository.urlEncode()).replace("{branch}", branch.urlEncode())
272+
call.respondRedirect(permanent = false, url = url)
273+
}
274+
264275
override suspend fun RoutingContext.postRepositoryBranch(
265276
repository: String,
266277
branch: String,

mps-sync-plugin3/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
testImplementation(kotlin("test"))
5555
testImplementation(project(":authorization"), excludeMPSLibraries)
5656
testImplementation(project(":model-server"), excludeMPSLibraries)
57+
testImplementation(libs.ktor.client.cio, excludeMPSLibraries)
5758
}
5859

5960
tasks {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.modelix.mps.sync3.ui
2+
3+
import com.intellij.ide.BrowserUtil
4+
import com.intellij.openapi.actionSystem.AnAction
5+
import com.intellij.openapi.actionSystem.AnActionEvent
6+
import com.intellij.openapi.project.Project
7+
import org.modelix.model.client2.ModelClientV2
8+
import org.modelix.mps.sync3.IModelSyncService
9+
10+
class OpenFrontendAction() : AnAction("Open Frontend in Browser") {
11+
12+
fun getFrontendUrls(project: Project): List<String> {
13+
val service = IModelSyncService.getInstance(project)
14+
15+
return service.getBindings().filter { it.isEnabled() }.map {
16+
ModelClientV2.builder().url(it.getConnection().getUrl()).build()
17+
.getFrontendUrl(it.getBranchRef()).toString()
18+
}
19+
}
20+
21+
override fun update(e: AnActionEvent) {
22+
e.presentation.isEnabledAndVisible = e.project?.let { getFrontendUrls(it).isNotEmpty() } ?: false
23+
}
24+
25+
override fun actionPerformed(e: AnActionEvent) {
26+
for (url in getFrontendUrls(e.project ?: return)) {
27+
BrowserUtil.open(url)
28+
}
29+
}
30+
}

mps-sync-plugin3/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<action id="org.modelix.mps.sync3.ui.ForceSyncToMPSAction" class="org.modelix.mps.sync3.ui.ForceSyncToMPSAction" icon="AllIcons.Vcs.Clone"/>
3636
<action id="org.modelix.mps.sync3.ui.ForceSyncToServerAction" class="org.modelix.mps.sync3.ui.ForceSyncToServerAction" icon="AllIcons.Vcs.Push"/>
3737
<action id="org.modelix.mps.sync3.ui.LogoutAction" class="org.modelix.mps.sync3.ui.LogoutAction" />
38+
<action id="org.modelix.mps.sync3.ui.OpenFrontendAction" class="org.modelix.mps.sync3.ui.OpenFrontendAction" />
3839
</group>
3940
</actions>
4041
</idea-plugin>

mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.modelix.mps.sync3
22

33
import com.intellij.configurationStore.saveSettings
4+
import io.ktor.client.HttpClient
5+
import io.ktor.client.engine.cio.CIO
46
import io.ktor.client.plugins.ResponseException
7+
import io.ktor.client.request.get
8+
import io.ktor.http.HttpStatusCode
59
import jetbrains.mps.smodel.SNodeUtil
610
import jetbrains.mps.smodel.adapter.ids.SConceptId
711
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId
@@ -38,7 +42,9 @@ import org.modelix.model.oauth.IAuthConfig
3842
import org.modelix.model.operations.IOperation
3943
import org.modelix.model.server.ModelServerPermissionSchema
4044
import org.modelix.mps.multiplatform.model.MPSIdGenerator
45+
import org.modelix.mps.sync3.ui.OpenFrontendAction
4146
import org.modelix.streams.getBlocking
47+
import java.net.URL
4248
import java.nio.file.Path
4349
import java.util.concurrent.atomic.AtomicLong
4450
import kotlin.io.path.readText
@@ -579,6 +585,44 @@ class ProjectSyncTest : MPSTestBase() {
579585
assertEquals(IBinding.Status.NoPermission(user = "[email protected]"), binding.getStatus())
580586
}
581587

588+
fun `test default frontend url`(): Unit = runWithModelServer { port ->
589+
val branchRef = RepositoryId("sync-test").getBranchReference("branchA")
590+
591+
openTestProject("initial")
592+
val binding = IModelSyncService.getInstance(mpsProject)
593+
.addServer("http://localhost:$port")
594+
.bind(branchRef, null)
595+
596+
val action = OpenFrontendAction()
597+
val urls = action.getFrontendUrls(project)
598+
assertEquals(listOf("http://localhost:$port/v2/repositories/sync-test/branches/branchA/frontend"), urls)
599+
600+
val response = HttpClient(CIO) {
601+
followRedirects = false
602+
}.get(urls.single())
603+
assertEquals(HttpStatusCode.Found, response.status)
604+
assertEquals("http://localhost:$port/history/sync-test/branchA/", URL(urls.single()).toURI().resolve(response.headers["Location"]).toString())
605+
}
606+
607+
fun `test configured frontend url`(): Unit = runWithModelServer(additionalEnv = mapOf("MODELIX_FRONTEND_URL" to "http://my-frontend.example.com/{repository}/{branch}/overview")) { port ->
608+
val branchRef = RepositoryId("sync-test").getBranchReference("branchA")
609+
610+
openTestProject("initial")
611+
val binding = IModelSyncService.getInstance(mpsProject)
612+
.addServer("http://localhost:$port")
613+
.bind(branchRef, null)
614+
615+
val action = OpenFrontendAction()
616+
val urls = action.getFrontendUrls(project)
617+
assertEquals(listOf("http://localhost:$port/v2/repositories/sync-test/branches/branchA/frontend"), urls)
618+
619+
val response = HttpClient(CIO) {
620+
followRedirects = false
621+
}.get(urls.single())
622+
assertEquals(HttpStatusCode.Found, response.status)
623+
assertEquals("http://my-frontend.example.com/sync-test/branchA/overview", URL(urls.single()).toURI().resolve(response.headers["Location"]).toString())
624+
}
625+
582626
fun `test loading enabled persisted binding`(): Unit = runPersistedBindingTest(true)
583627
fun `test loading disabled persisted binding`(): Unit = runPersistedBindingTest(false)
584628

mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/TestWithModelServer.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import kotlin.time.Duration.Companion.minutes
88
import kotlin.time.ExperimentalTime
99
import kotlin.time.toJavaDuration
1010

11-
fun runWithModelServer(hmacKey: String? = null, body: suspend (port: Int) -> Unit) = runBlocking {
11+
fun runWithModelServer(
12+
hmacKey: String? = null,
13+
additionalEnv: Map<String, String> = emptyMap(),
14+
body: suspend (port: Int) -> Unit,
15+
) = runBlocking {
1216
@OptIn(ExperimentalTime::class)
1317
withTimeout(5.minutes) {
1418
val modelServer: GenericContainer<*> = GenericContainer(System.getProperty("modelix.model.server.image"))
@@ -22,6 +26,7 @@ fun runWithModelServer(hmacKey: String? = null, body: suspend (port: Int) -> Uni
2226
it.withEnv("MODELIX_JWT_SIGNATURE_HMAC512_KEY", hmacKey)
2327
}
2428
}
29+
.withEnv(additionalEnv)
2530
.waitingFor(Wait.forListeningPort().withStartupTimeout(3.minutes.toJavaDuration()))
2631
.withLogConsumer {
2732
println(it.utf8StringWithoutLineEnding)

0 commit comments

Comments
 (0)