Admin customer detail: "Open Grafana drilldown" button
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=<customer-id> 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) <noreply@anthropic.com>
This commit is contained in:
parent
aaa522058e
commit
59c3f949d0
@ -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 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 { useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
fetchFleetCustomerDetail, type FleetSite, type FleetDevice,
|
fetchFleetCustomerDetail, type FleetSite, type FleetDevice,
|
||||||
type FleetRecentMeasurement, type FleetIngestEvent,
|
type FleetRecentMeasurement, type FleetIngestEvent,
|
||||||
} from '../api/fleet';
|
} from '../api/fleet';
|
||||||
|
import { fetchGrafanaConfig } from '../api/grafana';
|
||||||
|
|
||||||
const { Text } = Typography;
|
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() {
|
export function AdminCustomerDetailPage() {
|
||||||
const { id = '' } = useParams<{ id: string }>();
|
const { id = '' } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -18,6 +23,17 @@ export function AdminCustomerDetailPage() {
|
|||||||
queryFn: () => fetchFleetCustomerDetail(id),
|
queryFn: () => fetchFleetCustomerDetail(id),
|
||||||
refetchInterval: 30_000,
|
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 <div style={{ textAlign: 'center', padding: 64 }}><Spin size="large" /></div>;
|
if (isLoading) return <div style={{ textAlign: 'center', padding: 64 }}><Spin size="large" /></div>;
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
@ -56,11 +72,29 @@ export function AdminCustomerDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/customers')}>Customers</Button>
|
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/customers')}>Customers</Button>
|
||||||
<Text strong style={{ fontSize: 20 }}>{data.code} · {data.name}</Text>
|
<Text strong style={{ fontSize: 20 }}>{data.code} · {data.name}</Text>
|
||||||
{data.isActive ? <Tag color="green">Active</Tag> : <Tag color="red">Disabled</Tag>}
|
{data.isActive ? <Tag color="green">Active</Tag> : <Tag color="red">Disabled</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
drilldownUrl
|
||||||
|
? `Opens the Grafana customer-drilldown dashboard with var-customer=${data.id}`
|
||||||
|
: 'Grafana base URL is not configured for this Admin stack — see Settings → Grafana.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<LineChartOutlined />}
|
||||||
|
disabled={!drilldownUrl}
|
||||||
|
onClick={() => drilldownUrl && window.open(drilldownUrl, '_blank', 'noopener')}
|
||||||
|
>
|
||||||
|
Open Grafana drilldown
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Descriptions column={3} size="small">
|
<Descriptions column={3} size="small">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user