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) <noreply@anthropic.com>
This commit is contained in:
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user