fix: auto-compute environment slug + respect environment filter globally
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m26s
CI / docker (push) Successful in 1m6s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 16:01:50 +02:00
parent f95a78a380
commit cb36d7936f
11 changed files with 81 additions and 43 deletions

View File

@@ -46,7 +46,8 @@ public class RouteMetricsController {
public ResponseEntity<List<RouteMetrics>> getMetrics( public ResponseEntity<List<RouteMetrics>> getMetrics(
@RequestParam(required = false) String from, @RequestParam(required = false) String from,
@RequestParam(required = false) String to, @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 toInstant = to != null ? Instant.parse(to) : Instant.now();
Instant fromInstant = from != null ? Instant.parse(from) : toInstant.minus(24, ChronoUnit.HOURS); Instant fromInstant = from != null ? Instant.parse(from) : toInstant.minus(24, ChronoUnit.HOURS);
@@ -65,6 +66,9 @@ public class RouteMetricsController {
if (appId != null) { if (appId != null) {
sql.append(" AND application_id = " + lit(appId)); 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"); sql.append(" GROUP BY application_id, route_id ORDER BY application_id, route_id");
List<RouteMetrics> metrics = jdbc.query(sql.toString(), (rs, rowNum) -> { List<RouteMetrics> metrics = jdbc.query(sql.toString(), (rs, rowNum) -> {
@@ -91,11 +95,15 @@ public class RouteMetricsController {
for (int i = 0; i < metrics.size(); i++) { for (int i = 0; i < metrics.size(); i++) {
RouteMetrics m = metrics.get(i); RouteMetrics m = metrics.get(i);
try { 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, " + String sparkSql = "SELECT toStartOfInterval(bucket, toIntervalSecond(" + bucketSeconds + ")) AS period, " +
"COALESCE(countMerge(total_count), 0) AS cnt " + "COALESCE(countMerge(total_count), 0) AS cnt " +
"FROM stats_1m_route WHERE bucket >= " + lit(fromInstant) + " AND bucket < " + lit(toInstant) + sparkWhere + " GROUP BY period ORDER BY period";
" AND application_id = " + lit(m.appId()) + " AND route_id = " + lit(m.routeId()) +
" GROUP BY period ORDER BY period";
List<Double> sparkline = jdbc.query(sparkSql, List<Double> sparkline = jdbc.query(sparkSql,
(rs, rowNum) -> rs.getDouble("cnt")); (rs, rowNum) -> rs.getDouble("cnt"));
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(), metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
@@ -115,7 +123,7 @@ public class RouteMetricsController {
.map(AppSettings::slaThresholdMs).orElse(300); .map(AppSettings::slaThresholdMs).orElse(300);
Map<String, long[]> slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant, Map<String, long[]> slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant,
effectiveAppId, threshold, null); effectiveAppId, threshold, environment);
for (int i = 0; i < metrics.size(); i++) { for (int i = 0; i < metrics.size(); i++) {
RouteMetrics m = metrics.get(i); RouteMetrics m = metrics.get(i);
@@ -139,7 +147,8 @@ public class RouteMetricsController {
@RequestParam String routeId, @RequestParam String routeId,
@RequestParam(required = false) String appId, @RequestParam(required = false) String appId,
@RequestParam(required = false) Instant from, @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 toInstant = to != null ? to : Instant.now();
Instant fromInstant = from != null ? from : toInstant.minus(24, ChronoUnit.HOURS); Instant fromInstant = from != null ? from : toInstant.minus(24, ChronoUnit.HOURS);
@@ -161,6 +170,9 @@ public class RouteMetricsController {
if (appId != null) { if (appId != null) {
sql.append(" AND application_id = " + lit(appId)); 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(" GROUP BY processor_id, processor_type, route_id, application_id");
sql.append(" ORDER BY tc DESC"); sql.append(" ORDER BY tc DESC");

View File

@@ -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); const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['routes', 'metrics', from, to, appId], queryKey: ['routes', 'metrics', from, to, appId, environment],
queryFn: async () => { queryFn: async () => {
const token = useAuthStore.getState().accessToken; const token = useAuthStore.getState().accessToken;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (from) params.set('from', from); if (from) params.set('from', from);
if (to) params.set('to', to); if (to) params.set('to', to);
if (appId) params.set('appId', appId); if (appId) params.set('appId', appId);
if (environment) params.set('environment', environment);
const res = await fetch(`${config.apiBaseUrl}/routes/metrics?${params}`, { const res = await fetch(`${config.apiBaseUrl}/routes/metrics?${params}`, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View File

@@ -1,13 +1,14 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../client'; import { api } from '../client';
export function useCorrelationChain(correlationId: string | null) { export function useCorrelationChain(correlationId: string | null, environment?: string) {
return useQuery({ return useQuery({
queryKey: ['correlation-chain', correlationId], queryKey: ['correlation-chain', correlationId, environment],
queryFn: async () => { queryFn: async () => {
const { data } = await api.POST('/search/executions', { const { data } = await api.POST('/search/executions', {
body: { body: {
correlationId: correlationId!, correlationId: correlationId!,
environment,
limit: 20, limit: 20,
sortField: 'startTime', sortField: 'startTime',
sortDir: 'asc', sortDir: 'asc',

View File

@@ -3,15 +3,16 @@ import { config } from '../../config';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval'; 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); const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['processor-metrics', routeId, appId], queryKey: ['processor-metrics', routeId, appId, environment],
queryFn: async () => { queryFn: async () => {
const token = useAuthStore.getState().accessToken; const token = useAuthStore.getState().accessToken;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (routeId) params.set('routeId', routeId); if (routeId) params.set('routeId', routeId);
if (appId) params.set('appId', appId); if (appId) params.set('appId', appId);
if (environment) params.set('environment', environment);
const res = await fetch(`${config.apiBaseUrl}/routes/metrics/processors?${params}`, { const res = await fetch(`${config.apiBaseUrl}/routes/metrics/processors?${params}`, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View File

@@ -26,6 +26,14 @@ import {
import type { Environment } from '../../api/queries/admin/environments'; import type { Environment } from '../../api/queries/admin/environments';
import styles from './UserManagement.module.css'; 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() { export default function EnvironmentsPage() {
const { toast } = useToast(); const { toast } = useToast();
const { data: environments = [], isLoading } = useEnvironments(); const { data: environments = [], isLoading } = useEnvironments();
@@ -40,6 +48,10 @@ export default function EnvironmentsPage() {
const [newDisplayName, setNewDisplayName] = useState(''); const [newDisplayName, setNewDisplayName] = useState('');
const [newProduction, setNewProduction] = useState(false); const [newProduction, setNewProduction] = useState(false);
useEffect(() => {
setNewSlug(slugify(newDisplayName));
}, [newDisplayName]);
// Mutations // Mutations
const createEnv = useCreateEnvironment(); const createEnv = useCreateEnvironment();
const updateEnv = useUpdateEnvironment(); const updateEnv = useUpdateEnvironment();
@@ -153,19 +165,15 @@ export default function EnvironmentsPage() {
<> <>
{creating && ( {creating && (
<div className={styles.createForm}> <div className={styles.createForm}>
<Input
placeholder="Slug (e.g. staging) *"
value={newSlug}
onChange={(e) => setNewSlug(e.target.value)}
/>
{duplicateSlug && (
<span className={styles.errorText}>Slug already exists</span>
)}
<Input <Input
placeholder="Display name *" placeholder="Display name *"
value={newDisplayName} value={newDisplayName}
onChange={(e) => setNewDisplayName(e.target.value)} onChange={(e) => setNewDisplayName(e.target.value)}
/> />
<MonoText size="sm">{newSlug || '...'}</MonoText>
{duplicateSlug && (
<span className={styles.errorText}>Slug already exists</span>
)}
<label className={styles.securityRow}> <label className={styles.securityRow}>
<Toggle checked={newProduction} onChange={() => setNewProduction(!newProduction)} /> <Toggle checked={newProduction} onChange={() => setNewProduction(!newProduction)} />
Production environment Production environment

View File

@@ -12,6 +12,7 @@ import type { Column } from '@cameleer/design-system'
import { import {
useSearchExecutions, useSearchExecutions,
} from '../../api/queries/executions' } from '../../api/queries/executions'
import { useEnvironmentStore } from '../../api/environment-store'
import type { ExecutionSummary } from '../../api/types' import type { ExecutionSummary } from '../../api/types'
import { attributeBadgeColor } from '../../utils/attribute-color' import { attributeBadgeColor } from '../../utils/attribute-color'
import { formatDuration, statusLabel } from '../../utils/format-utils' import { formatDuration, statusLabel } from '../../utils/format-utils'
@@ -172,6 +173,7 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
if (activeExchangeId !== undefined) setSelectedId(activeExchangeId); if (activeExchangeId !== undefined) setSelectedId(activeExchangeId);
}, [activeExchangeId]); }, [activeExchangeId]);
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange, statusFilters } = useGlobalFilters() const { timeRange, statusFilters } = useGlobalFilters()
const timeFrom = timeRange.start.toISOString() const timeFrom = timeRange.start.toISOString()
const timeTo = timeRange.end.toISOString() const timeTo = timeRange.end.toISOString()
@@ -192,6 +194,7 @@ export default function Dashboard({ onExchangeSelect, activeExchangeId }: Dashbo
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
applicationId: appId || undefined, applicationId: appId || undefined,
environment: selectedEnv,
status: statusParam, status: statusParam,
text: textFilter, text: textFilter,
sortField, sortField,

View File

@@ -16,6 +16,7 @@ import { useGlobalFilters } from '@cameleer/design-system';
import { useRouteMetrics } from '../../api/queries/catalog'; import { useRouteMetrics } from '../../api/queries/catalog';
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useTimeseriesByApp, useTopErrors, useAllAppSettings, usePunchcard } from '../../api/queries/dashboard'; import { useTimeseriesByApp, useTopErrors, useAllAppSettings, usePunchcard } from '../../api/queries/dashboard';
import { useEnvironmentStore } from '../../api/environment-store';
import type { AppSettings } from '../../api/queries/dashboard'; import type { AppSettings } from '../../api/queries/dashboard';
import { Treemap } from './Treemap'; import { Treemap } from './Treemap';
import type { TreemapItem } from './Treemap'; import type { TreemapItem } from './Treemap';
@@ -290,17 +291,18 @@ function buildKpiItems(
export default function DashboardL1() { export default function DashboardL1() {
const navigate = useNavigate(); const navigate = useNavigate();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000; const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
const { data: metrics } = useRouteMetrics(timeFrom, timeTo); const { data: metrics } = useRouteMetrics(timeFrom, timeTo, undefined, selectedEnv);
const { data: stats } = useExecutionStats(timeFrom, timeTo); const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, undefined, selectedEnv);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, undefined, selectedEnv);
const { data: timeseriesByApp } = useTimeseriesByApp(timeFrom, timeTo); const { data: timeseriesByApp } = useTimeseriesByApp(timeFrom, timeTo, selectedEnv);
const { data: topErrors } = useTopErrors(timeFrom, timeTo); const { data: topErrors } = useTopErrors(timeFrom, timeTo, undefined, undefined, selectedEnv);
const { data: punchcardData } = usePunchcard(); const { data: punchcardData } = usePunchcard(undefined, selectedEnv);
const { data: allAppSettings } = useAllAppSettings(); const { data: allAppSettings } = useAllAppSettings();
// Build settings lookup map // Build settings lookup map

View File

@@ -20,6 +20,7 @@ import {
useAppSettings, useAppSettings,
usePunchcard, usePunchcard,
} from '../../api/queries/dashboard'; } from '../../api/queries/dashboard';
import { useEnvironmentStore } from '../../api/environment-store';
import type { TopError } from '../../api/queries/dashboard'; import type { TopError } from '../../api/queries/dashboard';
import { Treemap } from './Treemap'; import { Treemap } from './Treemap';
import type { TreemapItem } from './Treemap'; import type { TreemapItem } from './Treemap';
@@ -270,18 +271,19 @@ function buildKpiItems(
export default function DashboardL2() { export default function DashboardL2() {
const { appId } = useParams<{ appId: string }>(); const { appId } = useParams<{ appId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000; const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
// Data hooks // Data hooks
const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId); const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId, selectedEnv);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId, selectedEnv);
const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId); const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId, selectedEnv);
const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId); const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId, selectedEnv);
const { data: errors } = useTopErrors(timeFrom, timeTo, appId); const { data: errors } = useTopErrors(timeFrom, timeTo, appId, undefined, selectedEnv);
const { data: punchcardData } = usePunchcard(appId); const { data: punchcardData } = usePunchcard(appId, selectedEnv);
const { data: appSettings } = useAppSettings(appId); const { data: appSettings } = useAppSettings(appId);
const slaThresholdMs = appSettings?.slaThresholdMs ?? 300; const slaThresholdMs = appSettings?.slaThresholdMs ?? 300;

View File

@@ -14,6 +14,7 @@ import { useGlobalFilters } from '@cameleer/design-system';
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useProcessorMetrics } from '../../api/queries/processor-metrics'; import { useProcessorMetrics } from '../../api/queries/processor-metrics';
import { useTopErrors, useAppSettings } from '../../api/queries/dashboard'; import { useTopErrors, useAppSettings } from '../../api/queries/dashboard';
import { useEnvironmentStore } from '../../api/environment-store';
import type { TopError } from '../../api/queries/dashboard'; import type { TopError } from '../../api/queries/dashboard';
import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useDiagramByRoute } from '../../api/queries/diagrams';
import { ProcessDiagram } from '../../components/ProcessDiagram'; import { ProcessDiagram } from '../../components/ProcessDiagram';
@@ -244,16 +245,17 @@ function buildKpiItems(
export default function DashboardL3() { export default function DashboardL3() {
const { appId, routeId } = useParams<{ appId: string; routeId: string }>(); const { appId, routeId } = useParams<{ appId: string; routeId: string }>();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000; const windowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
// ── Data hooks ────────────────────────────────────────────────────────── // ── Data hooks ──────────────────────────────────────────────────────────
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId); const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId, selectedEnv);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId, selectedEnv);
const { data: processorMetrics } = useProcessorMetrics(routeId ?? null, appId); const { data: processorMetrics } = useProcessorMetrics(routeId ?? null, appId, selectedEnv);
const { data: topErrors } = useTopErrors(timeFrom, timeTo, appId, routeId); const { data: topErrors } = useTopErrors(timeFrom, timeTo, appId, routeId, selectedEnv);
const { data: diagramLayout } = useDiagramByRoute(appId, routeId); const { data: diagramLayout } = useDiagramByRoute(appId, routeId);
const { data: appSettings } = useAppSettings(appId); const { data: appSettings } = useAppSettings(appId);

View File

@@ -31,6 +31,7 @@ import { useDiagramByRoute } from '../../api/queries/diagrams';
import { useProcessorMetrics } from '../../api/queries/processor-metrics'; import { useProcessorMetrics } from '../../api/queries/processor-metrics';
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions'; import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
import { useApplicationConfig, useUpdateApplicationConfig, useTestExpression } from '../../api/queries/commands'; import { useApplicationConfig, useUpdateApplicationConfig, useTestExpression } from '../../api/queries/commands';
import { useEnvironmentStore } from '../../api/environment-store';
import type { TapDefinition } from '../../api/queries/commands'; import type { TapDefinition } from '../../api/queries/commands';
import type { ExecutionSummary } from '../../api/types'; import type { ExecutionSummary } from '../../api/types';
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
@@ -272,6 +273,7 @@ function buildDetailKpiItems(
export default function RouteDetail() { export default function RouteDetail() {
const { appId, routeId } = useParams(); const { appId, routeId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
@@ -305,16 +307,17 @@ export default function RouteDetail() {
}, []); }, []);
// ── API queries ──────────────────────────────────────────────────────────── // ── API queries ────────────────────────────────────────────────────────────
const { data: catalog } = useCatalog(); const { data: catalog } = useCatalog(selectedEnv);
const { data: diagram } = useDiagramByRoute(appId, routeId); const { data: diagram } = useDiagramByRoute(appId, routeId);
const { data: processorMetrics, isLoading: processorLoading } = useProcessorMetrics(routeId ?? null, appId); const { data: processorMetrics, isLoading: processorLoading } = useProcessorMetrics(routeId ?? null, appId, selectedEnv);
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId); const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId, selectedEnv);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId, selectedEnv);
const { data: recentResult, isLoading: recentLoading } = useSearchExecutions({ const { data: recentResult, isLoading: recentLoading } = useSearchExecutions({
timeFrom, timeFrom,
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
applicationId: appId || undefined, applicationId: appId || undefined,
environment: selectedEnv,
sortField: recentSortField, sortField: recentSortField,
sortDir: recentSortDir, sortDir: recentSortDir,
offset: 0, offset: 0,
@@ -325,6 +328,7 @@ export default function RouteDetail() {
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
applicationId: appId || undefined, applicationId: appId || undefined,
environment: selectedEnv,
status: 'FAILED', status: 'FAILED',
offset: 0, offset: 0,
limit: 200, limit: 200,

View File

@@ -15,6 +15,7 @@ import type { KpiItem, Column } from '@cameleer/design-system';
import { useGlobalFilters } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system';
import { useRouteMetrics } from '../../api/queries/catalog'; import { useRouteMetrics } from '../../api/queries/catalog';
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useEnvironmentStore } from '../../api/environment-store';
import type { RouteMetrics } from '../../api/types'; import type { RouteMetrics } from '../../api/types';
import styles from './RoutesMetrics.module.css'; import styles from './RoutesMetrics.module.css';
import tableStyles from '../../styles/table-section.module.css'; import tableStyles from '../../styles/table-section.module.css';
@@ -206,13 +207,14 @@ function buildKpiItems(
export default function RoutesMetrics() { export default function RoutesMetrics() {
const { appId } = useParams(); const { appId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId); const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId, selectedEnv);
const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId); const { data: stats } = useExecutionStats(timeFrom, timeTo, undefined, appId, selectedEnv);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId, selectedEnv);
// Map backend RouteMetrics[] to table rows // Map backend RouteMetrics[] to table rows
const rows: RouteRow[] = useMemo(() => const rows: RouteRow[] = useMemo(() =>