From 59c3f949d0067e5ddee55e31448c637ce682f188 Mon Sep 17 00:00:00 2001 From: Diseri Pearson Date: Mon, 18 May 2026 10:37:48 +0200 Subject: [PATCH] Admin customer detail: "Open Grafana drilldown" button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the existing customer-drilldown dashboard JSON to the customer detail page. Button opens ${Grafana.BaseUrl}/d/customer-drilldown in a new tab with var-customer= pre-filled, kiosk mode, light theme. - Fetches /api/grafana/config (cached 5min, reuses the existing TanStack query key so GrafanaInfoCard's cache is shared). - Button disabled with tooltip explaining when Grafana baseUrl isn't configured for the Admin stack (points to Settings → Grafana). - Customer id is URI-encoded before interpolation (defence in depth — it's a UUID, but encodeURIComponent costs nothing). - Dashboard UID hardcoded as 'customer-drilldown' to match the provisioned JSON. Renaming requires changing both together. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/pages/AdminCustomerDetailPage.tsx | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/portal/frontend/src/pages/AdminCustomerDetailPage.tsx b/portal/frontend/src/pages/AdminCustomerDetailPage.tsx index da9b9b0..90853a9 100644 --- a/portal/frontend/src/pages/AdminCustomerDetailPage.tsx +++ b/portal/frontend/src/pages/AdminCustomerDetailPage.tsx @@ -1,15 +1,20 @@ -import { Card, Descriptions, Tabs, Table, Tag, Typography, Button, Space, Spin, Result } from 'antd'; +import { Card, Descriptions, Tabs, Table, Tag, Typography, Button, Space, Spin, Result, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table'; -import { ArrowLeftOutlined } from '@ant-design/icons'; +import { ArrowLeftOutlined, LineChartOutlined } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; import { useNavigate, useParams } from 'react-router-dom'; import { fetchFleetCustomerDetail, type FleetSite, type FleetDevice, type FleetRecentMeasurement, type FleetIngestEvent, } from '../api/fleet'; +import { fetchGrafanaConfig } from '../api/grafana'; const { Text } = Typography; +// UID of the customer-drilldown dashboard provisioned in grafana/dashboards-admin/. +// Coordinated change: rename here and in the JSON together. +const CUSTOMER_DRILLDOWN_UID = 'customer-drilldown'; + export function AdminCustomerDetailPage() { const { id = '' } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -18,6 +23,17 @@ export function AdminCustomerDetailPage() { queryFn: () => fetchFleetCustomerDetail(id), refetchInterval: 30_000, }); + const { data: grafana } = useQuery({ + queryKey: ['grafana-config'], + queryFn: fetchGrafanaConfig, + staleTime: 5 * 60_000, + }); + + const drilldownUrl = (() => { + if (!data || !grafana?.baseUrl) return null; + const base = grafana.baseUrl.replace(/\/$/, ''); + return `${base}/d/${encodeURIComponent(CUSTOMER_DRILLDOWN_UID)}?orgId=1&kiosk=tv&theme=light&var-customer=${encodeURIComponent(data.id)}`; + })(); if (isLoading) return
; if (error || !data) { @@ -56,10 +72,28 @@ export function AdminCustomerDetailPage() { return ( - - - {data.code} · {data.name} - {data.isActive ? Active : Disabled} + + + + {data.code} · {data.name} + {data.isActive ? Active : Disabled} + + + +