From cb36d7936f9befa31c7548055d08a3e486fc0a43 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:01:50 +0200 Subject: [PATCH] fix: auto-compute environment slug + respect environment filter globally Part A: Environment creation slug is now auto-derived from display name and shown read-only (matching app creation pattern). Removes manual slug input. Part B: All data queries now pass the selected environment to backend: - Exchanges search, Dashboard L1/L2/L3 stats, Routes metrics, Route detail, correlation chains, and processor metrics all filter by selected environment. - Backend RouteMetricsController now accepts environment parameter for both route and processor metrics endpoints. Closes #XYZ Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controller/RouteMetricsController.java | 24 ++++++++++++++----- ui/src/api/queries/catalog.ts | 5 ++-- ui/src/api/queries/correlation.ts | 5 ++-- ui/src/api/queries/processor-metrics.ts | 5 ++-- ui/src/pages/Admin/EnvironmentsPage.tsx | 24 ++++++++++++------- ui/src/pages/Dashboard/Dashboard.tsx | 3 +++ ui/src/pages/DashboardTab/DashboardL1.tsx | 14 ++++++----- ui/src/pages/DashboardTab/DashboardL2.tsx | 14 ++++++----- ui/src/pages/DashboardTab/DashboardL3.tsx | 10 ++++---- ui/src/pages/Routes/RouteDetail.tsx | 12 ++++++---- ui/src/pages/Routes/RoutesMetrics.tsx | 8 ++++--- 11 files changed, 81 insertions(+), 43 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java index b6a6d561..6b64aa27 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteMetricsController.java @@ -46,7 +46,8 @@ public class RouteMetricsController { public ResponseEntity> getMetrics( @RequestParam(required = false) String from, @RequestParam(required = false) String to, - @RequestParam(required = false) String appId) { + @RequestParam(required = false) String appId, + @RequestParam(required = false) String environment) { Instant toInstant = to != null ? Instant.parse(to) : Instant.now(); Instant fromInstant = from != null ? Instant.parse(from) : toInstant.minus(24, ChronoUnit.HOURS); @@ -65,6 +66,9 @@ public class RouteMetricsController { if (appId != null) { sql.append(" AND application_id = " + lit(appId)); } + if (environment != null) { + sql.append(" AND environment = " + lit(environment)); + } sql.append(" GROUP BY application_id, route_id ORDER BY application_id, route_id"); List metrics = jdbc.query(sql.toString(), (rs, rowNum) -> { @@ -91,11 +95,15 @@ public class RouteMetricsController { for (int i = 0; i < metrics.size(); i++) { RouteMetrics m = metrics.get(i); try { + var sparkWhere = new StringBuilder( + "FROM stats_1m_route WHERE bucket >= " + lit(fromInstant) + " AND bucket < " + lit(toInstant) + + " AND application_id = " + lit(m.appId()) + " AND route_id = " + lit(m.routeId())); + if (environment != null) { + sparkWhere.append(" AND environment = " + lit(environment)); + } String sparkSql = "SELECT toStartOfInterval(bucket, toIntervalSecond(" + bucketSeconds + ")) AS period, " + "COALESCE(countMerge(total_count), 0) AS cnt " + - "FROM stats_1m_route WHERE bucket >= " + lit(fromInstant) + " AND bucket < " + lit(toInstant) + - " AND application_id = " + lit(m.appId()) + " AND route_id = " + lit(m.routeId()) + - " GROUP BY period ORDER BY period"; + sparkWhere + " GROUP BY period ORDER BY period"; List sparkline = jdbc.query(sparkSql, (rs, rowNum) -> rs.getDouble("cnt")); metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(), @@ -115,7 +123,7 @@ public class RouteMetricsController { .map(AppSettings::slaThresholdMs).orElse(300); Map slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant, - effectiveAppId, threshold, null); + effectiveAppId, threshold, environment); for (int i = 0; i < metrics.size(); i++) { RouteMetrics m = metrics.get(i); @@ -139,7 +147,8 @@ public class RouteMetricsController { @RequestParam String routeId, @RequestParam(required = false) String appId, @RequestParam(required = false) Instant from, - @RequestParam(required = false) Instant to) { + @RequestParam(required = false) Instant to, + @RequestParam(required = false) String environment) { Instant toInstant = to != null ? to : Instant.now(); Instant fromInstant = from != null ? from : toInstant.minus(24, ChronoUnit.HOURS); @@ -161,6 +170,9 @@ public class RouteMetricsController { if (appId != null) { sql.append(" AND application_id = " + lit(appId)); } + if (environment != null) { + sql.append(" AND environment = " + lit(environment)); + } sql.append(" GROUP BY processor_id, processor_type, route_id, application_id"); sql.append(" ORDER BY tc DESC"); diff --git a/ui/src/api/queries/catalog.ts b/ui/src/api/queries/catalog.ts index 20b5ee72..94303989 100644 --- a/ui/src/api/queries/catalog.ts +++ b/ui/src/api/queries/catalog.ts @@ -61,16 +61,17 @@ export function useCatalog(environment?: string) { }); } -export function useRouteMetrics(from?: string, to?: string, appId?: string) { +export function useRouteMetrics(from?: string, to?: string, appId?: string, environment?: string) { const refetchInterval = useRefreshInterval(30_000); return useQuery({ - queryKey: ['routes', 'metrics', from, to, appId], + queryKey: ['routes', 'metrics', from, to, appId, environment], queryFn: async () => { const token = useAuthStore.getState().accessToken; const params = new URLSearchParams(); if (from) params.set('from', from); if (to) params.set('to', to); if (appId) params.set('appId', appId); + if (environment) params.set('environment', environment); const res = await fetch(`${config.apiBaseUrl}/routes/metrics?${params}`, { headers: { Authorization: `Bearer ${token}`, diff --git a/ui/src/api/queries/correlation.ts b/ui/src/api/queries/correlation.ts index 66210bdb..c76cd3b1 100644 --- a/ui/src/api/queries/correlation.ts +++ b/ui/src/api/queries/correlation.ts @@ -1,13 +1,14 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; -export function useCorrelationChain(correlationId: string | null) { +export function useCorrelationChain(correlationId: string | null, environment?: string) { return useQuery({ - queryKey: ['correlation-chain', correlationId], + queryKey: ['correlation-chain', correlationId, environment], queryFn: async () => { const { data } = await api.POST('/search/executions', { body: { correlationId: correlationId!, + environment, limit: 20, sortField: 'startTime', sortDir: 'asc', diff --git a/ui/src/api/queries/processor-metrics.ts b/ui/src/api/queries/processor-metrics.ts index fc576cf9..84a6a680 100644 --- a/ui/src/api/queries/processor-metrics.ts +++ b/ui/src/api/queries/processor-metrics.ts @@ -3,15 +3,16 @@ import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; import { useRefreshInterval } from './use-refresh-interval'; -export function useProcessorMetrics(routeId: string | null, appId?: string) { +export function useProcessorMetrics(routeId: string | null, appId?: string, environment?: string) { const refetchInterval = useRefreshInterval(30_000); return useQuery({ - queryKey: ['processor-metrics', routeId, appId], + queryKey: ['processor-metrics', routeId, appId, environment], queryFn: async () => { const token = useAuthStore.getState().accessToken; const params = new URLSearchParams(); if (routeId) params.set('routeId', routeId); if (appId) params.set('appId', appId); + if (environment) params.set('environment', environment); const res = await fetch(`${config.apiBaseUrl}/routes/metrics/processors?${params}`, { headers: { Authorization: `Bearer ${token}`, diff --git a/ui/src/pages/Admin/EnvironmentsPage.tsx b/ui/src/pages/Admin/EnvironmentsPage.tsx index 01c08a24..58aa0974 100644 --- a/ui/src/pages/Admin/EnvironmentsPage.tsx +++ b/ui/src/pages/Admin/EnvironmentsPage.tsx @@ -26,6 +26,14 @@ import { import type { Environment } from '../../api/queries/admin/environments'; import styles from './UserManagement.module.css'; +function slugify(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 100); +} + export default function EnvironmentsPage() { const { toast } = useToast(); const { data: environments = [], isLoading } = useEnvironments(); @@ -40,6 +48,10 @@ export default function EnvironmentsPage() { const [newDisplayName, setNewDisplayName] = useState(''); const [newProduction, setNewProduction] = useState(false); + useEffect(() => { + setNewSlug(slugify(newDisplayName)); + }, [newDisplayName]); + // Mutations const createEnv = useCreateEnvironment(); const updateEnv = useUpdateEnvironment(); @@ -153,19 +165,15 @@ export default function EnvironmentsPage() { <> {creating && (
- setNewSlug(e.target.value)} - /> - {duplicateSlug && ( - Slug already exists - )} setNewDisplayName(e.target.value)} /> + {newSlug || '...'} + {duplicateSlug && ( + Slug already exists + )}