Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8b75c07
Add admin dashboard module with sample application
adrw Feb 12, 2026
d185d32
Add new admin dashboard tabs and responsive sidebar
adrw Feb 12, 2026
4cf8423
Enhance admin dashboard with endpoint reflection, collapsible section…
adrw Feb 12, 2026
031fcb4
Add *.class to .gitignore and clean up extracted JAR files
adrw Feb 12, 2026
c3c2df1
Auto-discover admin dashboard data from features and DI at runtime
adrw Feb 12, 2026
b140171
Add serverName config to admin dashboard for display customization
adrw Feb 12, 2026
8f084f3
Add KDocs feature, effective config view, and sidebar improvements
adrw Feb 12, 2026
5740ceb
Simplify admin dashboard welcome title to "Welcome to <service>!"
adrw Feb 12, 2026
f6ec339
Add documentation dropdowns, module tabs, and switch deps to compileOnly
adrw Feb 12, 2026
4147955
Fix detekt issues, add collapsible DB schema, and add module READMEs
adrw Feb 12, 2026
7d20fc9
Rename kairo-admin-sample to sample-kairo-admin and add Details dropd…
adrw Feb 12, 2026
88511e0
Refine endpoint UI: move fields to details dropdown, fix nested Stimu…
adrw Feb 12, 2026
89d7c4b
Add JSON editor with syntax highlighting, validation, and schema chec…
adrw Feb 12, 2026
7afba33
Add shareable URLs, keyboard shortcuts, and grouped endpoint dropdown
adrw Feb 12, 2026
6e197b7
Add permalink result caching and grouped table dropdown for database …
adrw Feb 13, 2026
0acf85d
Add auth support to admin dashboard via AuthReceiver extension functions
adrw Feb 13, 2026
7e04a88
Add Escape key to blur focused form fields in admin dashboard
adrw Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add KDocs feature, effective config view, and sidebar improvements
- Add kairo-kdocs module to serve Dokka-generated KDoc HTML
- Auto-detect KDocs availability and add sidebar link
- Show merged effective config on the config page
- Refactor sidebar nav links and external links to use sorted lists

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
adrw and claude committed Feb 12, 2026
commit 8f084f355c5e074c645100f88333e4587986899c
9 changes: 9 additions & 0 deletions buildSrc/src/main/kotlin/kairo-service-dokka.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("org.jetbrains.dokka")
}

tasks.register<Sync>("packageKdocs") {
dependsOn(rootProject.tasks.named("dokkaGenerate"))
from(rootProject.layout.buildDirectory.dir("dokka/html"))
into(layout.buildDirectory.dir("resources/main/static/kdocs"))
}
2 changes: 2 additions & 0 deletions kairo-admin-sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("kairo-library")
id("kairo-service-dokka")
application
}

Expand All @@ -12,6 +13,7 @@ tasks.withType<Zip> { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }

dependencies {
implementation(project(":kairo-admin"))
implementation(project(":kairo-kdocs"))
implementation(project(":kairo-application"))
implementation(project(":kairo-config"))
implementation(project(":kairo-dependency-injection:feature"))
Expand Down
2 changes: 2 additions & 0 deletions kairo-admin-sample/src/main/kotlin/kairo/adminSample/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kairo.adminSample

import kairo.admin.AdminDashboardConfig
import kairo.admin.AdminDashboardFeature
import kairo.kdocs.KdocsFeature
import kairo.adminSample.author.AuthorFeature
import kairo.adminSample.libraryBook.LibraryBookFeature
import kairo.application.kairo
Expand Down Expand Up @@ -36,6 +37,7 @@ public fun main(): Unit = kairo {
HealthCheckFeature(),
LibraryBookFeature(koin),
AuthorFeature(koin),
KdocsFeature(),
AdminDashboardFeature(
config = AdminDashboardConfig(
serverName = "Kairo Admin Sample",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public data class AdminDashboardConfig(
val docsUrl: String? = null,
val apiDocsUrl: String? = null,
val githubRepoUrl: String? = null,
val kdocsUrl: String? = null,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package kairo.admin

import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import io.ktor.server.application.Application
import io.r2dbc.spi.ConnectionFactory
import kairo.admin.collector.ConfigCollector
Expand Down Expand Up @@ -61,11 +63,14 @@ public class AdminDashboardFeature(

val resolvedConfigSources = configSources ?: autoDetectConfigSources()
val resolvedIntegrations = integrations ?: autoDetectIntegrations(connectionFactory)
val resolvedConfig = resolveConfig(config)

val effectiveConfig = generateEffectiveConfig(resolvedConfigSources)

adminDashboardHandler = AdminDashboardHandler(
config = config,
config = resolvedConfig,
endpointCollector = EndpointCollector { ktorApplication!!.restEndpointClasses.toList() },
configCollector = ConfigCollector(resolvedConfigSources),
configCollector = ConfigCollector(resolvedConfigSources, effectiveConfig),
jvmCollector = JvmCollector(),
databaseCollector = DatabaseCollector(connectionFactory),
poolCollector = PoolCollector(connectionFactory),
Expand Down Expand Up @@ -107,6 +112,14 @@ private fun discoverConnectionFactory(koin: Koin?): () -> ConnectionFactory? = {
}
}

private fun resolveConfig(config: AdminDashboardConfig): AdminDashboardConfig {
if (config.kdocsUrl != null) return config
val kdocsAvailable = Thread.currentThread().contextClassLoader
.getResource("static/kdocs/index.html") != null
if (!kdocsAvailable) return config
return config.copy(kdocsUrl = "/_kdocs/index.html")
}

private fun autoDetectConfigSources(): List<AdminConfigSource> {
val sources = mutableListOf<AdminConfigSource>()
val cl = Thread.currentThread().contextClassLoader
Expand All @@ -122,6 +135,22 @@ private fun autoDetectConfigSources(): List<AdminConfigSource> {
return sources
}

@Suppress("SwallowedException")
private fun generateEffectiveConfig(sources: List<AdminConfigSource>): String? =
try {
val renderOptions = ConfigRenderOptions.defaults()
.setOriginComments(false)
.setComments(false)
.setJson(false)
var merged = ConfigFactory.empty()
for (source in sources) {
merged = ConfigFactory.parseString(source.content).withFallback(merged)
}
merged.resolve().root().render(renderOptions)
} catch (_: Exception) {
null
}

private fun autoDetectIntegrations(
connectionFactory: () -> ConnectionFactory?,
): List<AdminIntegrationInfo> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import kairo.admin.AdminConfigSource

public class ConfigCollector(
private val configSources: List<AdminConfigSource>,
private val effectiveConfig: String? = null,
) {
public fun collect(): List<AdminConfigSource> = configSources

public fun effectiveConfig(): String? = effectiveConfig
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,10 @@ internal class AdminDashboardHandler(
private fun Route.configRoutes() {
get("/config") {
val sources = configCollector.collect()
val effectiveConfig = configCollector.effectiveConfig()
call.respondHtml {
adminLayout(config, "config") {
configView(config, sources, sources.firstOrNull()?.name)
}
}
}

get("/config/{name}") {
val name = call.parameters["name"].orEmpty()
val sources = configCollector.collect()
call.respondHtml {
adminLayout(config, "config") {
configView(config, sources, name)
configView(config, sources, effectiveConfig)
}
}
}
Expand Down
37 changes: 21 additions & 16 deletions kairo-admin/src/main/kotlin/kairo/admin/view/AdminLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,28 +105,33 @@ internal fun HTML.adminLayout(
div {
classes = setOf("space-y-1")
tabLink("Home", "", activeTab, config)
tabLink("Config", "config", activeTab, config)
tabLink("Database", "database", activeTab, config)
tabLink("Dependencies", "dependencies", activeTab, config)
tabLink("Endpoints", "endpoints", activeTab, config)
tabLink("Errors", "errors", activeTab, config)
tabLink("Features", "features", activeTab, config)
tabLink("Health", "health", activeTab, config)
tabLink("Integrations", "integrations", activeTab, config)
tabLink("JVM", "jvm", activeTab, config)
tabLink("Logging", "logging", activeTab, config)
listOf(
"Config" to "config",
"Database" to "database",
"Dependencies" to "dependencies",
"Endpoints" to "endpoints",
"Errors" to "errors",
"Features" to "features",
"Health" to "health",
"Integrations" to "integrations",
"JVM" to "jvm",
"Logging" to "logging",
).sortedBy { it.first }.forEach { (label, tab) ->
tabLink(label, tab, activeTab, config)
}
}
if (config.docsUrl != null || config.apiDocsUrl != null) {
if (config.docsUrl != null || config.apiDocsUrl != null || config.kdocsUrl != null) {
hr {
classes = setOf("border-gray-700", "my-4")
}
div {
classes = setOf("space-y-1")
if (config.docsUrl != null) {
externalLink("Docs", config.docsUrl)
}
if (config.apiDocsUrl != null) {
externalLink("Kairo Docs", config.apiDocsUrl)
listOfNotNull(
config.docsUrl?.let { "Docs" to it },
config.apiDocsUrl?.let { "Kairo Docs" to it },
config.kdocsUrl?.let { "KDocs" to it },
).sortedBy { it.first }.forEach { (label, url) ->
externalLink(label, url)
}
}
}
Expand Down
69 changes: 40 additions & 29 deletions kairo-admin/src/main/kotlin/kairo/admin/view/ConfigView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,71 @@ package kairo.admin.view
import kairo.admin.AdminConfigSource
import kairo.admin.AdminDashboardConfig
import kotlinx.html.FlowContent
import kotlinx.html.a
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.h2
import kotlinx.html.p
import kotlinx.html.pre

@Suppress("LongMethod")
internal fun FlowContent.configView(
config: AdminDashboardConfig,
@Suppress("UnusedParameter") config: AdminDashboardConfig,
sources: List<AdminConfigSource>,
selectedName: String?,
effectiveConfig: String?,
) {
h1 {
classes = setOf("text-2xl", "font-semibold", "text-gray-900", "mb-6")
+"Config"
}
if (sources.isEmpty()) {
if (sources.isEmpty() && effectiveConfig == null) {
p {
classes = setOf("text-gray-500")
+"No config sources registered."
}
return
}
div {
classes = setOf("flex", "gap-6")
// Sidebar: config file tabs.
div {
classes = setOf("w-48", "flex-shrink-0", "space-y-1")
sources.forEach { source ->
val isActive = source.name == selectedName
a(href = "${config.pathPrefix}/config/${source.name}") {
classes = if (isActive) {
setOf("block", "px-3", "py-2", "rounded-md", "bg-indigo-50", "text-indigo-700", "font-medium", "text-sm")
} else {
setOf("block", "px-3", "py-2", "rounded-md", "text-gray-600", "hover:bg-gray-100", "text-sm")
}
+source.name
classes = setOf("space-y-8")
// Effective Config section.
if (effectiveConfig != null) {
div {
h2 {
classes = setOf("text-xl", "font-semibold", "text-gray-900", "mb-3")
+"Effective Config"
}
pre {
classes = setOf(
"bg-gray-100",
"text-gray-900",
"p-4",
"rounded-lg",
"overflow-auto",
"text-sm",
"font-mono",
)
+effectiveConfig
}
}
}
// Content: selected config file.
div {
classes = setOf("flex-1", "bg-white", "rounded-lg", "shadow-sm", "p-6")
val selected = sources.find { it.name == selectedName }
if (selected != null) {
pre {
classes = setOf("bg-gray-100", "text-gray-900", "p-4", "rounded-lg", "overflow-auto", "text-sm", "font-mono")
+selected.content
// Individual config source files.
sources.forEach { source ->
div {
h2 {
classes = setOf("text-xl", "font-semibold", "text-gray-900", "mb-3")
+source.name
}
} else {
p {
classes = setOf("text-gray-500")
+"Select a config file."
pre {
classes = setOf(
"bg-gray-100",
"text-gray-900",
"p-4",
"rounded-lg",
"overflow-auto",
"text-sm",
"font-mono",
)
+source.content
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions kairo-kdocs/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
id("kairo-library")
id("kairo-library-publish")
}

dependencies {
compileOnly(project(":kairo-feature"))
compileOnly(project(":kairo-rest"))

implementation(libs.ktorServer)
}
6 changes: 6 additions & 0 deletions kairo-kdocs/src/main/kotlin/kairo/kdocs/KdocsConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kairo.kdocs

public data class KdocsConfig(
val pathPrefix: String = "/_kdocs",
val resourcePath: String = "static/kdocs",
)
34 changes: 34 additions & 0 deletions kairo-kdocs/src/main/kotlin/kairo/kdocs/KdocsFeature.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kairo.kdocs

import io.ktor.server.application.Application
import io.ktor.server.http.content.staticResources
import io.ktor.server.routing.routing
import kairo.feature.Feature
import kairo.rest.HasRouting

/**
* Serves Dokka-generated KDocs from the classpath as static resources.
* If no KDocs resources are found on the classpath, the feature is a no-op.
*
* To populate the classpath with KDocs, apply the `kairo-service-dokka` convention plugin
* to your application module and run `./gradlew dokkaGenerate :your-app:packageKdocs`.
*/
public class KdocsFeature(
public val config: KdocsConfig = KdocsConfig(),
) : Feature(), HasRouting {
override val name: String = "KDocs"

/**
* Whether KDocs resources are available on the classpath.
*/
public val available: Boolean =
Thread.currentThread().contextClassLoader
.getResource("${config.resourcePath}/index.html") != null

override fun Application.routing() {
if (!available) return
routing {
staticResources(config.pathPrefix, config.resourcePath)
}
}
}
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ include(":kairo-validation")
include(":kairo-admin")
include(":kairo-admin-sample")
include(":kairo-admin-sample:db")

include(":kairo-kdocs")