diff --git a/ui/package-lock.json b/ui/package-lock.json index 185ecf1b..10e510ab 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,7 +8,7 @@ "name": "ui", "version": "0.0.0", "dependencies": { - "@cameleer/design-system": "^0.1.14", + "@cameleer/design-system": "^0.1.15", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", @@ -276,9 +276,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.14", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.14/design-system-0.1.14.tgz", - "integrity": "sha512-kdWdpep9odlQoHCfUU98ZlMzM98tfu9LWOweCYnUR53V9aAdeytZcB2hEP7waIJrrFAGgjD/62s3sLkrzl1LXA==", + "version": "0.1.15", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.15/design-system-0.1.15.tgz", + "integrity": "sha512-+Lt9uu6jKQ+BsJRWwxwdYR5xdDMiDVxopNOE9kiUwiu1GOjEs1s/jz7rnjXwqsuMb9zOZADwu9MLLpoYeivJpw==", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index 2d8b45be..015ecbe0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts" }, "dependencies": { - "@cameleer/design-system": "^0.1.14", + "@cameleer/design-system": "^0.1.15", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", diff --git a/ui/src/pages/Admin/AppConfigPage.module.css b/ui/src/pages/Admin/AppConfigPage.module.css index e16a5e76..fab6cac3 100644 --- a/ui/src/pages/Admin/AppConfigPage.module.css +++ b/ui/src/pages/Admin/AppConfigPage.module.css @@ -76,6 +76,24 @@ font-size: 11px; } +.routeLabel { + font-size: 11px; + color: var(--text-secondary); +} + +.tapBadgeLink { + background: none; + border: none; + padding: 0; + cursor: pointer; + border-radius: var(--radius-sm); + transition: opacity 0.15s; +} + +.tapBadgeLink:hover { + opacity: 0.75; +} + .removeBtn { background: none; border: none; @@ -90,8 +108,36 @@ color: var(--error); } +.panelToolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + .panelMeta { font-size: 11px; color: var(--text-muted); - margin-bottom: 12px; } + +.panelActions { + display: flex; + gap: 8px; + align-items: center; +} + +.editBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 14px; + padding: 4px 6px; + border-radius: var(--radius-sm); + transition: color 0.15s; +} + +.editBtn:hover { + color: var(--text-primary); +} + diff --git a/ui/src/pages/Admin/AppConfigPage.tsx b/ui/src/pages/Admin/AppConfigPage.tsx index 88e5eda0..cd06c0b1 100644 --- a/ui/src/pages/Admin/AppConfigPage.tsx +++ b/ui/src/pages/Admin/AppConfigPage.tsx @@ -1,4 +1,6 @@ import { useState, useMemo, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { useQueries } from '@tanstack/react-query'; import { DataTable, Badge, MonoText, DetailPanel, SectionHeader, Button, Toggle, Spinner, useToast, } from '@cameleer/design-system'; @@ -6,6 +8,7 @@ import type { Column } from '@cameleer/design-system'; import { useAllApplicationConfigs, useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands'; import { useRouteCatalog } from '../../api/queries/catalog'; +import { api } from '../../api/client'; import type { AppCatalogEntry, RouteSummary } from '../../api/types'; import styles from './AppConfigPage.module.css'; @@ -68,6 +71,7 @@ function buildColumns(): Column[] { function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => void }) { const { toast } = useToast(); + const navigate = useNavigate(); const { data: config, isLoading } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); const { data: catalog } = useRouteCatalog(); @@ -83,6 +87,33 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi return entry?.routes ?? []; }, [catalog, appId]); + // Fetch diagrams for all routes to build processorId → routeId mapping + const diagramQueries = useQueries({ + queries: appRoutes.map((r) => ({ + queryKey: ['diagrams', 'byRoute', appId, r.routeId], + queryFn: async () => { + const { data, error } = await api.GET('/diagrams', { + params: { query: { application: appId!, routeId: r.routeId } }, + }); + if (error) return { routeId: r.routeId, nodes: [] as Array<{ id?: string }> }; + return { routeId: r.routeId, nodes: (data as any)?.nodes ?? [] }; + }, + enabled: !!appId, + staleTime: 60_000, + })), + }); + + const processorToRoute = useMemo(() => { + const map: Record = {}; + for (const q of diagramQueries) { + if (!q.data) continue; + for (const node of q.data.nodes) { + if (node.id) map[node.id] = q.data.routeId; + } + } + return map; + }, [diagramQueries.map(q => q.data)]); + useEffect(() => { if (config) { setForm({ @@ -137,6 +168,16 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi }); } + function navigateToTaps(processorId: string) { + const routeId = processorToRoute[processorId]; + onClose(); + if (routeId) { + navigate(`/routes/${appId}/${routeId}?tab=taps`); + } else { + navigate(`/routes/${appId}`); + } + } + // ── Traces & Taps merged rows const tracedTapRows: TracedTapRow[] = useMemo(() => { const traced = editing ? tracedDraft : (config?.tracedProcessors ?? {}); @@ -149,6 +190,10 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi const tapCount = config?.taps?.length ?? 0; const tracedTapColumns: Column[] = useMemo(() => [ + { key: 'route' as any, header: 'Route', render: (_v, row) => { + const routeId = processorToRoute[row.processorId]; + return routeId ? {routeId} : ; + }}, { key: 'processorId', header: 'Processor', render: (_v, row) => {row.processorId} }, { key: 'captureMode', header: 'Capture', @@ -166,7 +211,11 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi key: 'taps', header: 'Taps', render: (_v, row) => row.taps.length === 0 ? - :
{row.taps.map(t => )}
, + :
{row.taps.map(t => ( + + ))}
, }, ...(editing ? [{ key: '_remove' as const, header: '', width: '36px', @@ -174,7 +223,7 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi ), }] : []), - ], [editing]); + ], [editing, processorToRoute]); // ── Route Recording rows const routeRecordingRows: RouteRecordingRow[] = useMemo(() => { @@ -194,9 +243,21 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi return ( <> -
- Version {config.version} - {config.updatedAt && <> · Updated {timeAgo(config.updatedAt)}} +
+
+ Version {config.version} + {config.updatedAt && <> · Updated {timeAgo(config.updatedAt)}} +
+ {editing ? ( +
+ + +
+ ) : ( + + )}
{/* Settings */} @@ -227,25 +288,25 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi ? updateField('metricsEnabled', (e.target as HTMLInputElement).checked)} /> : }
-
- Sampling Rate - {editing - ? updateField('samplingRate', parseFloat(e.target.value) || 0)} /> - : {form.samplingRate}} -
Compress Success {editing ? updateField('compressSuccess', (e.target as HTMLInputElement).checked)} /> : }
+
+ Sampling Rate + {editing + ? updateField('samplingRate', parseFloat(e.target.value) || 0)} /> + : {form.samplingRate}} +
{/* Traces & Taps */}
Traces & Taps
- {tracedCount} traced · {tapCount} taps · manage taps on route pages + {tracedCount} traced · {tapCount} taps · click tap to manage {tracedTapRows.length > 0 ? columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> : No processor traces or taps configured.} diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx index 3eb0a8d4..bb37ca5a 100644 --- a/ui/src/pages/Routes/RouteDetail.tsx +++ b/ui/src/pages/Routes/RouteDetail.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, useCallback } from 'react'; -import { useParams, useNavigate, Link } from 'react-router'; +import { useParams, useNavigate, useSearchParams, Link } from 'react-router'; import { KpiStrip, Badge, @@ -270,7 +270,8 @@ export default function RouteDetail() { const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); - const [activeTab, setActiveTab] = useState('performance'); + const [searchParams] = useSearchParams(); + const [activeTab, setActiveTab] = useState(searchParams.get('tab') || 'performance'); const [recentSortField, setRecentSortField] = useState('startTime'); const [recentSortDir, setRecentSortDir] = useState<'asc' | 'desc'>('desc');