Skip to content

Commit fac5946

Browse files
feat(database-ui): static react app calling on database json api
1 parent f1ea23d commit fac5946

35 files changed

+7590
-825
lines changed

packages/cli/src/main/kotlin/elide/tool/cli/cmd/db/DbStudioCommand.kt

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ import io.micronaut.core.annotation.ReflectiveAccess
1818
import picocli.CommandLine.Command
1919
import picocli.CommandLine.Option
2020
import picocli.CommandLine.Parameters
21-
import java.nio.file.FileSystems
2221
import java.nio.file.Files
2322
import java.nio.file.Path
2423
import java.nio.file.StandardCopyOption
25-
import java.nio.file.attribute.BasicFileAttributes
2624
import kotlin.io.path.exists
2725
import kotlin.io.path.fileSize
2826
import kotlin.io.path.getLastModifiedTime
@@ -33,7 +31,6 @@ import kotlin.io.path.name
3331
import kotlin.io.path.readText
3432
import kotlin.io.path.writeText
3533
import kotlinx.serialization.Serializable
36-
import kotlinx.serialization.encodeToString
3734
import kotlinx.serialization.json.Json
3835
import elide.tool.cli.AbstractSubcommand
3936
import elide.tool.cli.CommandContext
@@ -73,7 +70,8 @@ data class DiscoveredDatabase(
7370
internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>() {
7471

7572
private companion object {
76-
private const val STUDIO_RESOURCE_PATH = "db-studio"
73+
private const val STUDIO_API_SOURCE = "samples/db-studio/api"
74+
private const val STUDIO_UI_SOURCE = "samples/db-studio/ui/dist"
7775
private const val STUDIO_OUTPUT_DIR = ".db-studio"
7876
private const val STUDIO_INDEX_FILE = "index.tsx"
7977
private val SQLITE_EXTENSIONS = setOf(".db", ".sqlite", ".sqlite3", ".db3")
@@ -158,30 +156,21 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
158156
}
159157
}
160158

161-
private fun copyResourceDirectory(resourcePath: String, targetDir: Path) {
162-
val resourceUrl = this::class.java.classLoader.getResource(resourcePath)
163-
?: error("Resource not found: $resourcePath")
164-
165-
val uri = resourceUrl.toURI()
166-
val fileSystem = when (uri.scheme) {
167-
"jar" -> FileSystems.newFileSystem(uri, emptyMap<String, Any>())
168-
else -> null
159+
private fun copyDirectory(sourcePath: Path, targetDir: Path) {
160+
if (!sourcePath.exists() || !sourcePath.isDirectory()) {
161+
error("Source directory not found: $sourcePath")
169162
}
170163

171-
fileSystem.use {
172-
val sourcePath = fileSystem?.getPath(resourcePath) ?: Path.of(uri)
173-
174-
Files.walk(sourcePath).use { stream ->
175-
stream.forEach { source ->
176-
val relative = sourcePath.relativize(source)
177-
val target = targetDir.resolve(relative.toString())
164+
Files.walk(sourcePath).use { stream ->
165+
stream.forEach { source ->
166+
val relative = sourcePath.relativize(source)
167+
val target = targetDir.resolve(relative.toString())
178168

179-
when {
180-
source.isDirectory() -> Files.createDirectories(target)
181-
else -> {
182-
Files.createDirectories(target.parent)
183-
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING)
184-
}
169+
when {
170+
source.isDirectory() -> Files.createDirectories(target)
171+
else -> {
172+
Files.createDirectories(target.parent)
173+
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING)
185174
}
186175
}
187176
}
@@ -203,58 +192,97 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
203192
)
204193
internal var port: Int = 4983
205194

195+
@Option(
196+
names = ["--api-port"],
197+
description = ["Port to run the API server on"],
198+
defaultValue = "4984",
199+
)
200+
internal var apiPort: Int = 4984
201+
206202
internal var host: String = "localhost"
207203

208204
override suspend fun CommandContext.invoke(state: ToolContext<ToolState>): CommandResult {
209205
val outputDir = Path.of(STUDIO_OUTPUT_DIR)
210-
copyResourceDirectory(STUDIO_RESOURCE_PATH, outputDir)
211206

212-
val indexFile = outputDir.resolve(STUDIO_INDEX_FILE)
207+
// Create organized directory structure
208+
val apiDir = outputDir.resolve("api")
209+
val uiDir = outputDir.resolve("ui")
210+
Files.createDirectories(apiDir)
211+
Files.createDirectories(uiDir)
212+
213+
// Copy API server files from samples/db-studio/api to .db-studio/api/
214+
val apiSource = Path.of(STUDIO_API_SOURCE)
215+
if (!apiSource.exists() || !apiSource.isDirectory()) {
216+
return CommandResult.err(
217+
message = "API source not found at $STUDIO_API_SOURCE. Ensure samples/db-studio/api exists."
218+
)
219+
}
220+
copyDirectory(apiSource, apiDir)
221+
222+
// Copy React app UI files from samples/db-studio/ui/dist to .db-studio/ui/
223+
val uiSource = Path.of(STUDIO_UI_SOURCE)
224+
if (!uiSource.exists() || !uiSource.isDirectory()) {
225+
return CommandResult.err(
226+
message = "UI build not found at $STUDIO_UI_SOURCE. Please run 'cd samples/db-studio/ui && npm run build' first."
227+
)
228+
}
229+
copyDirectory(uiSource, uiDir)
230+
231+
val indexFile = apiDir.resolve(STUDIO_INDEX_FILE)
213232
val baseContent = indexFile.readText()
214233

215-
// Handle database selection mode vs direct database path
216-
val processedContent = if (databasePath == null) {
217-
// Discovery mode: find available databases
234+
// Always use databases array - either discover or create from single path
235+
val databases = if (databasePath == null) {
218236
val discovered = discoverDatabases()
219237

220238
if (discovered.isEmpty()) {
221239
return CommandResult.err(message = "No SQLite databases found in current directory or user data directories")
222240
}
223241

224-
val json = Json { prettyPrint = false }
225-
val databasesJson = json.encodeToString(discovered)
226-
227-
baseContent
228-
.replace("\"__DB_PATH__\"", "null")
229-
.replace("__PORT__", port.toString())
230-
.replace("\"__DATABASES__\"", databasesJson)
231-
.replace("__SELECTION_MODE__", "true")
242+
discovered
232243
} else {
233-
// Direct path mode: use provided database
234244
val dbFile = Path.of(databasePath!!)
235245

236246
if (!dbFile.exists()) {
237247
return CommandResult.err(message = "Database file not found: $databasePath")
238248
}
239249

240-
val absoluteDbPath = dbFile.toAbsolutePath().toString()
241-
242-
baseContent
243-
.replace("\"__DB_PATH__\"", "\"$absoluteDbPath\"")
244-
.replace("__PORT__", port.toString())
245-
.replace("\"__DATABASES__\"", "[]")
246-
.replace("__SELECTION_MODE__", "false")
250+
// Create a single-item list with the specified database
251+
listOf(
252+
DiscoveredDatabase(
253+
path = dbFile.toAbsolutePath().toString(),
254+
name = dbFile.name,
255+
size = dbFile.fileSize(),
256+
lastModified = dbFile.getLastModifiedTime().toMillis(),
257+
isLocal = true,
258+
)
259+
)
247260
}
248261

262+
val json = Json { prettyPrint = false }
263+
val databasesJson = json.encodeToString(databases)
264+
265+
val processedContent = baseContent
266+
.replace("__PORT__", apiPort.toString())
267+
.replace("\"__DATABASES__\"", databasesJson)
268+
249269
indexFile.writeText(processedContent)
250270

251271
output {
252-
appendLine("Generated database studio in: ${outputDir.toAbsolutePath()}")
272+
appendLine("Database Studio files generated in: ${outputDir.toAbsolutePath()}")
273+
appendLine()
274+
appendLine("To start the Database Studio:")
275+
appendLine()
276+
appendLine(" Terminal 1 (API Server on port $apiPort):")
277+
appendLine(" cd ${apiDir.toAbsolutePath()}")
278+
appendLine(" elide serve $STUDIO_INDEX_FILE")
279+
appendLine()
280+
appendLine(" Terminal 2 (UI Server on port 8080):")
281+
appendLine(" cd ${uiDir.toAbsolutePath()}")
282+
appendLine(" elide serve .")
253283
appendLine()
254-
appendLine("To start the database UI, run:")
255-
appendLine("elide serve $STUDIO_OUTPUT_DIR/$STUDIO_INDEX_FILE")
284+
appendLine("Then open: http://localhost:8080")
256285
appendLine()
257-
appendLine("Then open: http://$host:$port")
258286
}
259287

260288
return CommandResult.success()

0 commit comments

Comments
 (0)