From 8de4c819229e72cd93584f15bdb17a7e236e5136 Mon Sep 17 00:00:00 2001 From: Jeff Hwang Date: Thu, 28 Aug 2025 13:50:54 -0400 Subject: [PATCH 1/4] render service info --- .../dashboard/BackfilaWebActionsModule.kt | 1 + .../dashboard/GetServiceDetailsAction.kt | 58 ++++++ .../ui/components/ServiceInfoModal.kt | 178 ++++++++++++++++++ .../backfila/ui/pages/ServiceShowAction.kt | 12 ++ 4 files changed, 249 insertions(+) create mode 100644 service/src/main/kotlin/app/cash/backfila/dashboard/GetServiceDetailsAction.kt create mode 100644 service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt diff --git a/service/src/main/kotlin/app/cash/backfila/dashboard/BackfilaWebActionsModule.kt b/service/src/main/kotlin/app/cash/backfila/dashboard/BackfilaWebActionsModule.kt index 97ff0adb9..6b8a731df 100644 --- a/service/src/main/kotlin/app/cash/backfila/dashboard/BackfilaWebActionsModule.kt +++ b/service/src/main/kotlin/app/cash/backfila/dashboard/BackfilaWebActionsModule.kt @@ -14,6 +14,7 @@ class BackfilaWebActionsModule() : KAbstractModule() { install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) + install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) diff --git a/service/src/main/kotlin/app/cash/backfila/dashboard/GetServiceDetailsAction.kt b/service/src/main/kotlin/app/cash/backfila/dashboard/GetServiceDetailsAction.kt new file mode 100644 index 000000000..d863896f2 --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/dashboard/GetServiceDetailsAction.kt @@ -0,0 +1,58 @@ +package app.cash.backfila.dashboard + +import app.cash.backfila.service.persistence.BackfilaDb +import app.cash.backfila.service.persistence.ServiceQuery +import java.time.Instant +import javax.inject.Inject +import misk.exceptions.BadRequestException +import misk.hibernate.Query +import misk.hibernate.Transacter +import misk.hibernate.newQuery +import misk.security.authz.Authenticated +import misk.web.Get +import misk.web.PathParam +import misk.web.ResponseContentType +import misk.web.actions.WebAction +import misk.web.mediatype.MediaTypes + +data class GetServiceDetailsResponse( + val service_name: String, + val variant: String, + val connector: String, + val connector_extra_data: String?, + val slack_channel: String?, + val created_at: Instant, + val updated_at: Instant, + val last_registered_at: Instant?, +) + +class GetServiceDetailsAction @Inject constructor( + @BackfilaDb private val transacter: Transacter, + private val queryFactory: Query.Factory, +) : WebAction { + @Get("/services/{service}/variants/{variant}/details") + @ResponseContentType(MediaTypes.APPLICATION_JSON) + @Authenticated(allowAnyUser = true) + fun getServiceDetails( + @PathParam service: String, + @PathParam variant: String, + ): GetServiceDetailsResponse { + return transacter.transaction { session -> + val dbService = queryFactory.newQuery() + .registryName(service) + .variant(variant) + .uniqueResult(session) ?: throw BadRequestException("`$service`-`$variant` doesn't exist") + + GetServiceDetailsResponse( + service_name = dbService.registry_name, + variant = dbService.variant, + connector = dbService.connector, + connector_extra_data = dbService.connector_extra_data, + slack_channel = dbService.slack_channel, + created_at = dbService.created_at, + updated_at = dbService.updated_at, + last_registered_at = dbService.last_registered_at, + ) + } + } +} diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt new file mode 100644 index 000000000..5767e9874 --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt @@ -0,0 +1,178 @@ +package app.cash.backfila.ui.components + +import kotlinx.html.ButtonType +import kotlinx.html.TagConsumer +import kotlinx.html.button +import kotlinx.html.div +import kotlinx.html.id +import kotlinx.html.script +import kotlinx.html.span +import kotlinx.html.unsafe + +fun TagConsumer<*>.ServiceInfoModal( + serviceName: String, + variantName: String, +) { + div("fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 hidden") { + id = "service-info-modal" + + div("relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white") { + div("flex items-center justify-between pb-3 border-b border-gray-200") { + span("text-lg font-semibold text-gray-900") { + +"Service Information" + } + button(classes = "text-gray-400 hover:text-gray-600") { + type = ButtonType.button + id = "close-modal-btn" + unsafe { + +""" + + """ + } + } + } + + div("mt-4") { + div("space-y-4") { + div { + span("text-sm font-medium text-gray-700") { +"Service Name:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-service-name" + +serviceName + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Variant:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-variant" + +variantName + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Connector:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-connector" + +"Loading..." + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Connector Extra Data:" } + div("mt-1 text-sm text-gray-900 font-mono bg-gray-50 p-2 rounded border max-h-32 overflow-y-auto") { + id = "modal-connector-extra-data" + +"Loading..." + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Slack Channel:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-slack-channel" + +"Loading..." + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Created At:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-created-at" + +"Loading..." + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Updated At:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-updated-at" + +"Loading..." + } + } + + div { + span("text-sm font-medium text-gray-700") { +"Last Registered At:" } + div("mt-1 text-sm text-gray-900") { + id = "modal-last-registered-at" + +"Loading..." + } + } + } + } + + div("flex justify-end pt-4 border-t border-gray-200 mt-6") { + button(classes = "px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400") { + type = ButtonType.button + id = "close-modal-footer-btn" + +"Close" + } + } + } + } + + script { + unsafe { + +""" + function initServiceInfoModal() { + const modal = document.getElementById('service-info-modal'); + const closeBtn = document.getElementById('close-modal-btn'); + const closeFooterBtn = document.getElementById('close-modal-footer-btn'); + + function closeModal() { + modal.classList.add('hidden'); + } + + function openModal() { + modal.classList.remove('hidden'); + loadServiceDetails(); + } + + function loadServiceDetails() { + const url = '/services/$serviceName/variants/$variantName/details'; + fetch(url) + .then(response => response.json()) + .then(data => { + document.getElementById('modal-connector').textContent = data.connector || 'N/A'; + document.getElementById('modal-connector-extra-data').textContent = data.connector_extra_data || 'None'; + document.getElementById('modal-slack-channel').textContent = data.slack_channel || 'None'; + document.getElementById('modal-created-at').textContent = new Date(data.created_at).toLocaleString(); + document.getElementById('modal-updated-at').textContent = new Date(data.updated_at).toLocaleString(); + document.getElementById('modal-last-registered-at').textContent = + data.last_registered_at ? new Date(data.last_registered_at).toLocaleString() : 'Never'; + }) + .catch(err => { + console.error('Error loading service details:', err); + document.getElementById('modal-connector').textContent = 'Error loading data'; + document.getElementById('modal-connector-extra-data').textContent = 'Error loading data'; + document.getElementById('modal-slack-channel').textContent = 'Error loading data'; + document.getElementById('modal-created-at').textContent = 'Error loading data'; + document.getElementById('modal-updated-at').textContent = 'Error loading data'; + document.getElementById('modal-last-registered-at').textContent = 'Error loading data'; + }); + } + + // Event listeners + if (closeBtn) closeBtn.addEventListener('click', closeModal); + if (closeFooterBtn) closeFooterBtn.addEventListener('click', closeModal); + + // Close modal when clicking outside + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeModal(); + } + }); + + // Expose function globally so the info button can call it + window.openServiceInfoModal = openModal; + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initServiceInfoModal); + } else { + initServiceInfoModal(); + } + """.trimIndent() + } + } +} diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt index 8119dcab1..a1e7fbfeb 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt @@ -7,6 +7,7 @@ import app.cash.backfila.ui.components.BackfillsTable import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle import app.cash.backfila.ui.components.PaginationWithHistory +import app.cash.backfila.ui.components.ServiceInfoModal import java.net.HttpURLConnection import javax.inject.Inject import javax.inject.Singleton @@ -79,6 +80,14 @@ class ServiceShowAction @Inject constructor( ) .buildHtmlResponseBody { PageTitle("Service", label) { + // Service info button + button(classes = "rounded-full bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600 mr-2") { + type = ButtonType.button + attributes["onclick"] = "window.openServiceInfoModal && window.openServiceInfoModal()" + +"""Info""" + } + + // Create button a { href = BackfillCreateServiceIndexAction.path(service, variantOrBlank) @@ -91,6 +100,9 @@ class ServiceShowAction @Inject constructor( BackfillSearchForm(backfill_name, created_by_user, service, variant) + // Add the service info modal + ServiceInfoModal(service, variant) + AutoReload(frameId = "backfill-$service-status") { BackfillsTable(true, backfillRuns.running_backfills) BackfillsTable(false, backfillRuns.paused_backfills, showDeleted) From 4cd1990b6ae5fdf12116f67ff9ac673c33c7d140 Mon Sep 17 00:00:00 2001 From: Jeff Hwang Date: Thu, 28 Aug 2025 15:10:27 -0400 Subject: [PATCH 2/4] linkable page --- .../kotlin/app/cash/backfila/ui/UiModule.kt | 2 + .../backfila/ui/pages/ServiceInfoAction.kt | 179 ++++++++++++++++++ .../backfila/ui/pages/ServiceShowAction.kt | 17 +- 3 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceInfoAction.kt diff --git a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt index 0a06f593b..6080f520c 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt @@ -10,6 +10,7 @@ import app.cash.backfila.ui.pages.BackfillIndexAction import app.cash.backfila.ui.pages.BackfillShowAction import app.cash.backfila.ui.pages.IndexAction import app.cash.backfila.ui.pages.ServiceIndexAction +import app.cash.backfila.ui.pages.ServiceInfoAction import app.cash.backfila.ui.pages.ServiceShowAction import misk.inject.KAbstractModule import misk.web.WebActionModule @@ -20,6 +21,7 @@ class UiModule : KAbstractModule() { install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) + install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceInfoAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceInfoAction.kt new file mode 100644 index 000000000..c4e4bf959 --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceInfoAction.kt @@ -0,0 +1,179 @@ +package app.cash.backfila.ui.pages + +import app.cash.backfila.dashboard.GetServiceDetailsAction +import app.cash.backfila.ui.components.DashboardPageLayout +import app.cash.backfila.ui.components.PageTitle +import java.net.HttpURLConnection +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.html.div +import kotlinx.html.pre +import kotlinx.html.span +import misk.security.authz.Authenticated +import misk.tailwind.Link +import misk.web.Get +import misk.web.PathParam +import misk.web.Response +import misk.web.ResponseBody +import misk.web.ResponseContentType +import misk.web.actions.WebAction +import misk.web.mediatype.MediaTypes +import misk.web.toResponseBody + +@Singleton +class ServiceInfoAction @Inject constructor( + private val dashboardPageLayout: DashboardPageLayout, + private val getServiceDetailsAction: GetServiceDetailsAction, +) : WebAction { + + @Get(PATH) + @ResponseContentType(MediaTypes.TEXT_HTML) + @Authenticated(capabilities = ["users"]) + fun get( + @PathParam service: String?, + @PathParam variantOrBlank: String? = "", + ): Response { + if (service.isNullOrBlank()) { + return Response( + body = "Service name is required".toResponseBody(), + statusCode = HttpURLConnection.HTTP_BAD_REQUEST, + ) + } + + val variant = variantOrBlank.orEmpty().ifBlank { "default" } + + try { + val serviceDetails = getServiceDetailsAction.getServiceDetails(service, variant) + val label = if (variant == "default") service else "$service ($variant)" + + val htmlResponseBody = dashboardPageLayout.newBuilder() + .title("$label Info | Backfila") + .breadcrumbLinks( + Link("Services", ServiceIndexAction.PATH), + Link(label, ServiceShowAction.path(service, variantOrBlank)), + Link("Info", path(service, variantOrBlank)), + ) + .buildHtmlResponseBody { + PageTitle("Service Information", label) + + div("bg-white shadow rounded-lg") { + div("px-4 py-5 sm:p-6") { + div("space-y-6") { + div { + span("text-sm font-medium text-gray-500") { +"Service Name" } + div("mt-1 text-sm text-gray-900 font-semibold") { + +serviceDetails.service_name + } + } + + div { + span("text-sm font-medium text-gray-500") { +"Variant" } + div("mt-1 text-sm text-gray-900") { + +serviceDetails.variant + } + } + + div { + span("text-sm font-medium text-gray-500") { +"Connector" } + div("mt-1 text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded") { + +serviceDetails.connector + } + } + + div { + span("text-sm font-medium text-gray-500") { +"Connector Extra Data" } + div("mt-1") { + if (serviceDetails.connector_extra_data.isNullOrBlank()) { + div("text-sm text-gray-500 italic") { +"None" } + } else { + pre("text-sm text-gray-900 bg-gray-50 p-3 rounded border overflow-x-auto whitespace-pre-wrap") { + +serviceDetails.connector_extra_data + } + } + } + } + + div { + span("text-sm font-medium text-gray-500") { +"Slack Channel" } + div("mt-1 text-sm text-gray-900") { + if (serviceDetails.slack_channel.isNullOrBlank()) { + span("text-gray-500 italic") { +"None configured" } + } else { + span("font-mono bg-blue-100 text-blue-800 px-2 py-1 rounded") { + +serviceDetails.slack_channel + } + } + } + } + + // Timestamps section + div("border-t border-gray-200 pt-6") { + span("text-sm font-medium text-gray-500 block mb-4") { +"Timestamps" } + + div("grid grid-cols-1 md:grid-cols-3 gap-4") { + div { + span("text-xs font-medium text-gray-500") { +"Created At" } + div("mt-1 text-sm text-gray-900") { + +serviceDetails.created_at.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + } + } + + div { + span("text-xs font-medium text-gray-500") { +"Updated At" } + div("mt-1 text-sm text-gray-900") { + +serviceDetails.updated_at.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + } + } + + div { + span("text-xs font-medium text-gray-500") { +"Last Registered At" } + div("mt-1 text-sm text-gray-900") { + if (serviceDetails.last_registered_at != null) { + +serviceDetails.last_registered_at.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + } else { + span("text-red-600 font-medium") { +"Never registered" } + } + } + } + } + } + } + } + } + } + + return Response(htmlResponseBody) + } catch (e: Exception) { + val htmlResponseBody = dashboardPageLayout.newBuilder() + .title("Service Info Error | Backfila") + .breadcrumbLinks( + Link("Services", ServiceIndexAction.PATH), + ) + .buildHtmlResponseBody { + PageTitle("Service Information", "Error") + + div("bg-red-50 border border-red-200 rounded-md p-4") { + div("flex") { + div("ml-3") { + span("text-sm font-medium text-red-800") { +"Error loading service information" } + div("mt-2 text-sm text-red-700") { + +e.message.orEmpty() + } + } + } + } + } + + return Response(htmlResponseBody) + } + } + + companion object { + private const val PATH = "/services/{service}/{variantOrBlank}/info" + fun path(service: String, variantOrBlank: String?) = PATH + .replace("{service}", service) + .replace("{variantOrBlank}", variantOrBlank ?: "") + } +} diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt index a1e7fbfeb..a347b2fbe 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt @@ -7,7 +7,6 @@ import app.cash.backfila.ui.components.BackfillsTable import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle import app.cash.backfila.ui.components.PaginationWithHistory -import app.cash.backfila.ui.components.ServiceInfoModal import java.net.HttpURLConnection import javax.inject.Inject import javax.inject.Singleton @@ -80,11 +79,14 @@ class ServiceShowAction @Inject constructor( ) .buildHtmlResponseBody { PageTitle("Service", label) { - // Service info button - button(classes = "rounded-full bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600 mr-2") { - type = ButtonType.button - attributes["onclick"] = "window.openServiceInfoModal && window.openServiceInfoModal()" - +"""Info""" + // Service info button - now links to dedicated page + a { + href = ServiceInfoAction.path(service, variantOrBlank) + + button(classes = "rounded-full bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600 mr-2") { + type = ButtonType.button + +"""Info""" + } } // Create button @@ -100,9 +102,6 @@ class ServiceShowAction @Inject constructor( BackfillSearchForm(backfill_name, created_by_user, service, variant) - // Add the service info modal - ServiceInfoModal(service, variant) - AutoReload(frameId = "backfill-$service-status") { BackfillsTable(true, backfillRuns.running_backfills) BackfillsTable(false, backfillRuns.paused_backfills, showDeleted) From 35ecf0e2db26bfbf237b1daf271ad85ba068c328 Mon Sep 17 00:00:00 2001 From: Jeff Hwang Date: Thu, 28 Aug 2025 15:17:50 -0400 Subject: [PATCH 3/4] remove modal not in use --- .../kotlin/app/cash/backfila/ui/UiModule.kt | 2 - .../ui/components/ServiceInfoModal.kt | 178 ------------------ 2 files changed, 180 deletions(-) delete mode 100644 service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt diff --git a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt index 6080f520c..0a06f593b 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt @@ -10,7 +10,6 @@ import app.cash.backfila.ui.pages.BackfillIndexAction import app.cash.backfila.ui.pages.BackfillShowAction import app.cash.backfila.ui.pages.IndexAction import app.cash.backfila.ui.pages.ServiceIndexAction -import app.cash.backfila.ui.pages.ServiceInfoAction import app.cash.backfila.ui.pages.ServiceShowAction import misk.inject.KAbstractModule import misk.web.WebActionModule @@ -21,7 +20,6 @@ class UiModule : KAbstractModule() { install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) - install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt deleted file mode 100644 index 5767e9874..000000000 --- a/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceInfoModal.kt +++ /dev/null @@ -1,178 +0,0 @@ -package app.cash.backfila.ui.components - -import kotlinx.html.ButtonType -import kotlinx.html.TagConsumer -import kotlinx.html.button -import kotlinx.html.div -import kotlinx.html.id -import kotlinx.html.script -import kotlinx.html.span -import kotlinx.html.unsafe - -fun TagConsumer<*>.ServiceInfoModal( - serviceName: String, - variantName: String, -) { - div("fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 hidden") { - id = "service-info-modal" - - div("relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white") { - div("flex items-center justify-between pb-3 border-b border-gray-200") { - span("text-lg font-semibold text-gray-900") { - +"Service Information" - } - button(classes = "text-gray-400 hover:text-gray-600") { - type = ButtonType.button - id = "close-modal-btn" - unsafe { - +""" - - """ - } - } - } - - div("mt-4") { - div("space-y-4") { - div { - span("text-sm font-medium text-gray-700") { +"Service Name:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-service-name" - +serviceName - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Variant:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-variant" - +variantName - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Connector:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-connector" - +"Loading..." - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Connector Extra Data:" } - div("mt-1 text-sm text-gray-900 font-mono bg-gray-50 p-2 rounded border max-h-32 overflow-y-auto") { - id = "modal-connector-extra-data" - +"Loading..." - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Slack Channel:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-slack-channel" - +"Loading..." - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Created At:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-created-at" - +"Loading..." - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Updated At:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-updated-at" - +"Loading..." - } - } - - div { - span("text-sm font-medium text-gray-700") { +"Last Registered At:" } - div("mt-1 text-sm text-gray-900") { - id = "modal-last-registered-at" - +"Loading..." - } - } - } - } - - div("flex justify-end pt-4 border-t border-gray-200 mt-6") { - button(classes = "px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400") { - type = ButtonType.button - id = "close-modal-footer-btn" - +"Close" - } - } - } - } - - script { - unsafe { - +""" - function initServiceInfoModal() { - const modal = document.getElementById('service-info-modal'); - const closeBtn = document.getElementById('close-modal-btn'); - const closeFooterBtn = document.getElementById('close-modal-footer-btn'); - - function closeModal() { - modal.classList.add('hidden'); - } - - function openModal() { - modal.classList.remove('hidden'); - loadServiceDetails(); - } - - function loadServiceDetails() { - const url = '/services/$serviceName/variants/$variantName/details'; - fetch(url) - .then(response => response.json()) - .then(data => { - document.getElementById('modal-connector').textContent = data.connector || 'N/A'; - document.getElementById('modal-connector-extra-data').textContent = data.connector_extra_data || 'None'; - document.getElementById('modal-slack-channel').textContent = data.slack_channel || 'None'; - document.getElementById('modal-created-at').textContent = new Date(data.created_at).toLocaleString(); - document.getElementById('modal-updated-at').textContent = new Date(data.updated_at).toLocaleString(); - document.getElementById('modal-last-registered-at').textContent = - data.last_registered_at ? new Date(data.last_registered_at).toLocaleString() : 'Never'; - }) - .catch(err => { - console.error('Error loading service details:', err); - document.getElementById('modal-connector').textContent = 'Error loading data'; - document.getElementById('modal-connector-extra-data').textContent = 'Error loading data'; - document.getElementById('modal-slack-channel').textContent = 'Error loading data'; - document.getElementById('modal-created-at').textContent = 'Error loading data'; - document.getElementById('modal-updated-at').textContent = 'Error loading data'; - document.getElementById('modal-last-registered-at').textContent = 'Error loading data'; - }); - } - - // Event listeners - if (closeBtn) closeBtn.addEventListener('click', closeModal); - if (closeFooterBtn) closeFooterBtn.addEventListener('click', closeModal); - - // Close modal when clicking outside - modal.addEventListener('click', function(e) { - if (e.target === modal) { - closeModal(); - } - }); - - // Expose function globally so the info button can call it - window.openServiceInfoModal = openModal; - } - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initServiceInfoModal); - } else { - initServiceInfoModal(); - } - """.trimIndent() - } - } -} From 69dcfa6c8e88f7bcebd0a20cb9b401c57a8266f5 Mon Sep 17 00:00:00 2001 From: Jeff Hwang Date: Thu, 28 Aug 2025 15:22:44 -0400 Subject: [PATCH 4/4] add serviceinfoaction to module --- service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt index 0a06f593b..6080f520c 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt @@ -10,6 +10,7 @@ import app.cash.backfila.ui.pages.BackfillIndexAction import app.cash.backfila.ui.pages.BackfillShowAction import app.cash.backfila.ui.pages.IndexAction import app.cash.backfila.ui.pages.ServiceIndexAction +import app.cash.backfila.ui.pages.ServiceInfoAction import app.cash.backfila.ui.pages.ServiceShowAction import misk.inject.KAbstractModule import misk.web.WebActionModule @@ -20,6 +21,7 @@ class UiModule : KAbstractModule() { install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) + install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create())