From fea35e8b3f5111896aac00912a46344309353105 Mon Sep 17 00:00:00 2001 From: Hyu Date: Fri, 9 Oct 2026 18:54:32 +0800 Subject: [PATCH] fix(acceptance): improve execution monitoring and processor navigation --- .../pkg/api/http/service/monitoring.py | 22 ++++ .../service/monitoring_execution_details.py | 60 ++++++++- src/langbot/pkg/pipeline/aggregator.py | 7 ++ src/langbot/pkg/pipeline/pipelinemgr.py | 5 +- .../agents/PluginProcessorDetailContent.tsx | 116 ++++++++++-------- .../components/PluginProcessorTrace.tsx | 26 ++-- .../agents/components/ProcessorRunList.tsx | 6 +- .../components/home-sidebar/HomeSidebar.tsx | 6 + .../ProcessorDetailWorkbench.tsx | 22 ++-- .../monitoring/components/TokenMonitoring.tsx | 2 +- .../overview-cards/TrafficChart.tsx | 4 +- .../home/monitoring/hooks/useExecutions.ts | 8 +- .../monitoring/hooks/useMonitoringData.ts | 6 +- web/src/app/home/monitoring/page.tsx | 18 +-- .../home/pipelines/PipelineDetailContent.tsx | 9 +- 15 files changed, 221 insertions(+), 96 deletions(-) diff --git a/src/langbot/pkg/api/http/service/monitoring.py b/src/langbot/pkg/api/http/service/monitoring.py index 653be4c93..23dadfa33 100644 --- a/src/langbot/pkg/api/http/service/monitoring.py +++ b/src/langbot/pkg/api/http/service/monitoring.py @@ -1418,6 +1418,28 @@ class MonitoringService(ExecutionDetailsMixin): end_time=end_time, debug=True if mode == 'debug' else False if mode == 'real' else None, ) + Message = persistence_monitoring.MonitoringMessage + pipeline_timing = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(AgentRun.started_at, AgentRun.finished_at) + .select_from(Message) + .join(AgentRun, sqlalchemy.and_( + AgentRun.workspace_id == workspace_uuid, + AgentRun.run_id == Message.run_id, + )) + .where( + *pipeline_conditions, + Message.status == 'success', + AgentRun.started_at.is_not(None), + AgentRun.finished_at.is_not(None), + ) + .order_by(Message.timestamp.desc()) + .limit(self._detail_limit()) + ) + durations.extend( + (row[1] - row[0]).total_seconds() * 1000 + for row in pipeline_timing.all() if row[0] and row[1] + ) + durations.sort() pipeline_status_result = await self.ap.persistence_mgr.execute_async( sqlalchemy.select( persistence_monitoring.MonitoringMessage.status, diff --git a/src/langbot/pkg/api/http/service/monitoring_execution_details.py b/src/langbot/pkg/api/http/service/monitoring_execution_details.py index d8496facc..9a4811e8d 100644 --- a/src/langbot/pkg/api/http/service/monitoring_execution_details.py +++ b/src/langbot/pkg/api/http/service/monitoring_execution_details.py @@ -201,10 +201,25 @@ class ExecutionDetailsMixin: # Absorbed steering events belong to an existing run, too. conditions = [ EventLog.workspace_id == workspace, + # The primary route is first; plugin subscriptions follow it. + # Delivered pipeline ingress is not an unhandled execution, including + # older queued/aggregated messages whose trace identity was lost. + ~sa.and_( + sa.func.coalesce( + self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), '' + ) == 'pipeline', + sa.func.coalesce( + self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'status')), '' + ) == 'delivered', + ), ~sa.exists( sa.select(AgentRun.id).where( AgentRun.workspace_id == workspace, - sa.or_(AgentRun.event_id == EventLog.event_id, AgentRun.run_id == EventLog.run_id), + sa.or_( + AgentRun.event_id == EventLog.event_id, + AgentRun.run_id == EventLog.run_id, + self._execution_json_text(AgentRun.metadata_json, 'ingress_event_id') == EventLog.event_id, + ), ) ), ~sa.exists( @@ -259,6 +274,49 @@ class ExecutionDetailsMixin: } async def enrich_execution_rows(self, workspace, items): + pipeline_items = {item['id']: item for item in items if item.get('source') == 'pipeline'} + if pipeline_items: + # Pipeline rows own the UI identity; their linked Runner ledger owns + # timing and usage. Read them in one batch for both list and detail. + linked = await self._execution_query( + workspace, + sa.select(models.MonitoringMessage.id, AgentRun) + .join(AgentRun, sa.and_( + AgentRun.workspace_id == workspace, + AgentRun.run_id == models.MonitoringMessage.run_id, + )) + .where( + models.MonitoringMessage.workspace_uuid == workspace, + models.MonitoringMessage.id.in_(pipeline_items), + ), + ) + for message_id, run in linked.all(): + metrics = self._serialize_agent_execution(run) + pipeline_items[message_id].update({ + key: metrics[key] + for key in ('started_at_ms', 'finished_at_ms', 'duration_ms', 'usage', 'cost') + }) + + calls = models.MonitoringLLMCall + usage_rows = await self._execution_query( + workspace, + sa.select( + calls.message_id, + sa.func.sum(calls.input_tokens), + sa.func.sum(calls.output_tokens), + sa.func.sum(calls.total_tokens), + ).where( + calls.workspace_uuid == workspace, + calls.message_id.in_(pipeline_items), + ).group_by(calls.message_id), + ) + for message_id, input_tokens, output_tokens, total_tokens in usage_rows.all(): + if not pipeline_items[message_id].get('usage'): + pipeline_items[message_id]['usage'] = { + 'input_tokens': int(input_tokens or 0), + 'output_tokens': int(output_tokens or 0), + 'total_tokens': int(total_tokens or 0), + } ids = {item['event_id'] for item in items if item.get('event_id')} if not ids: return diff --git a/src/langbot/pkg/pipeline/aggregator.py b/src/langbot/pkg/pipeline/aggregator.py index acf0a29f7..32ec748b6 100644 --- a/src/langbot/pkg/pipeline/aggregator.py +++ b/src/langbot/pkg/pipeline/aggregator.py @@ -162,6 +162,13 @@ class MessageAggregator: if variables: enabled = False + # Preserve ingress identity across the queue and detached debounce task. + from ..telemetry import trace + + ingress_trace = trace.current() + if ingress_trace is not None: + variables = {**(variables or {}), '_ingress_event_id': ingress_trace.event_id} + if not enabled: await self.ap.query_pool.add_query( bot_uuid=bot_uuid, diff --git a/src/langbot/pkg/pipeline/pipelinemgr.py b/src/langbot/pkg/pipeline/pipelinemgr.py index ad45b4c50..3928f49ee 100644 --- a/src/langbot/pkg/pipeline/pipelinemgr.py +++ b/src/langbot/pkg/pipeline/pipelinemgr.py @@ -423,12 +423,13 @@ class RuntimePipeline: token = processing_mode.set('pipeline') # The Workspace-scoped opaque query uuid is the execution identity Space # shows for this lane; it is a stable UUID for every pooled query. - execution_id = str(getattr(query, 'query_uuid', '') or '').strip() or str(query.query_id) + ingress_event_id = (query.variables or {}).get('_ingress_event_id') + execution_id = ingress_event_id or str(getattr(query, 'query_uuid', '') or '').strip() or str(query.query_id) # A lane with no inbound platform event in flight (WebChat, WebUI debug, # HTTP pipeline run) synthesizes its virtual inbound event here, so every # execution starts at an event boundary and the Runner is never the trace # origin. A lane running under a platform route nests inside that ingress. - synthesizing = trace_mod.current() is None + synthesizing = trace_mod.current() is None and not ingress_event_id try: with ingress( self.ap, diff --git a/web/src/app/home/agents/PluginProcessorDetailContent.tsx b/web/src/app/home/agents/PluginProcessorDetailContent.tsx index 79e7df1a0..0d848f097 100644 --- a/web/src/app/home/agents/PluginProcessorDetailContent.tsx +++ b/web/src/app/home/agents/PluginProcessorDetailContent.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useSearchParams } from 'react-router-dom'; import { eventPatternLabel } from '@/app/home/components/event-patterns/event-pattern-groups'; -import { RefreshCw, Trash2, ScrollText, Settings2 } from 'lucide-react'; +import { RefreshCw, Trash2, Clock3, Puzzle } from 'lucide-react'; import isEqual from 'lodash/isEqual'; import { toast } from 'sonner'; import type { @@ -29,6 +29,7 @@ import PluginProcessorTrace, { ProcessorPayload, } from './components/PluginProcessorTrace'; import ProcessorRunList from './components/ProcessorRunList'; +import { processorRunDuration, formatRunDuration } from './components/processor-run-timing'; import PluginProcessorSettings from './components/PluginProcessorSettings'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; @@ -52,10 +53,14 @@ export default function PluginProcessorDetailContent({ onSaved: () => void; }) { const { t } = useTranslation(); - const [searchParams] = useSearchParams(); - const [activeTab, setActiveTab] = useState( - searchParams.get('tab') === 'logs' ? 'logs' : 'config', - ); + const [, setSearchParams] = useSearchParams(); + const setActiveTab = useCallback((tab: string) => { + setSearchParams((current) => { + const next = new URLSearchParams(current); + next.set('tab', tab); + return next; + }, { preventScrollReset: true }); + }, [setSearchParams]); const [platformTools, setPlatformTools] = useState([]); const toolLabels = Object.fromEntries( platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]), @@ -135,7 +140,7 @@ export default function PluginProcessorDetailContent({ : []), { id: 'logs', - target: '[data-guide="event-processor-form-tab-logs"]', + target: '[data-guide="event-processor-form-monitoring"]', title: t('guidedTour.pluginProcessor.logs.title'), description: t('guidedTour.pluginProcessor.logs.description'), onEnter: () => setActiveTab('logs'), @@ -152,7 +157,7 @@ export default function PluginProcessorDetailContent({ ] : []), ], - [t, available, hasParameters, canOperate], + [t, available, hasParameters, canOperate, setActiveTab], ); const applyInstalledProcessor = useCallback((component: RunnerDescriptor) => { @@ -170,7 +175,7 @@ export default function PluginProcessorDetailContent({ ); setActiveTab('config'); validate.current = null; - }, []); + }, [setActiveTab]); const load = useCallback(async () => { setLoading(true); @@ -356,14 +361,7 @@ export default function PluginProcessorDetailContent({ } const logsContent = ( -
-
{ - event.preventDefault(); - void save(); - }} - /> +
{failed && ( @@ -377,16 +375,19 @@ export default function PluginProcessorDetailContent({ ({runs.length})
+
{runs.length > 0 && ( void openRun(run)} @@ -405,7 +406,7 @@ export default function PluginProcessorDetailContent({ /> )} -
+
{!selected ? ( @@ -421,25 +422,31 @@ export default function PluginProcessorDetailContent({ ) : ( <> -
-

+

+
+

{eventPatternLabel(selected.metadata.event_type ?? '', t)}

{new Date(selected.created_at * 1000).toLocaleString()}

+
+
+ {processorRunDuration(selected) !== null && ( + + + {formatRunDuration(processorRunDuration(selected)!)} + + )} + + {t(`agents.eventProcessor.status_${selected.status}`, { defaultValue: selected.status })} + +
- - {t(`agents.eventProcessor.status_${selected.status}`, { - defaultValue: selected.status, - })} - +
+

{t('agents.eventProcessor.input')}

{selected.metadata.delivery != null && ( @@ -448,7 +455,14 @@ export default function PluginProcessorDetailContent({ value={selected.metadata.delivery} /> )} - +
+
+

+ + {t('agents.eventProcessor.type')} · {t('agents.eventProcessor.logsTab')} +

+ +
{selected.status === 'failed' && selected.status_reason && ( @@ -469,6 +483,7 @@ export default function PluginProcessorDetailContent({ )}
+
); @@ -477,6 +492,10 @@ export default function PluginProcessorDetailContent({ return ( <> + { + event.preventDefault(); + void save(); + }} /> + + {t('agents.eventProcessor.type')} + + } titleAction={ canManage ? : undefined } @@ -529,15 +554,13 @@ export default function PluginProcessorDetailContent({ ) : undefined } configTitle={t('agents.eventProcessor.type')} - configTabs={{ - value: activeTab, - onValueChange: setActiveTab, - items: [ - { - value: 'config', - label: t('agents.eventProcessor.configTab'), - icon: , - content: ( + configIcon={} + monitoring={{ + label: t('pipelines.monitoring.title'), + workbenchLabel: t('pipelines.monitoring.workbench'), + content: logsContent, + }} + configContent={
{!component ? (

@@ -563,16 +586,7 @@ export default function PluginProcessorDetailContent({ )}

- ), - }, - { - value: 'logs', - label: t('agents.eventProcessor.logsTab'), - icon: , - content: logsContent, - }, - ], - }} + } debugTitle={canOperate ? t('agents.debugTab') : undefined} debugDescription={t('agents.eventProcessor.debugNotice')} debugContent={ diff --git a/web/src/app/home/agents/components/PluginProcessorTrace.tsx b/web/src/app/home/agents/components/PluginProcessorTrace.tsx index e37a247ad..c91335b87 100644 --- a/web/src/app/home/agents/components/PluginProcessorTrace.tsx +++ b/web/src/app/home/agents/components/PluginProcessorTrace.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { ChevronRight } from 'lucide-react'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import { ChevronRight, ScrollText } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { @@ -47,23 +46,25 @@ export default function PluginProcessorTrace({ }) { const { t } = useTranslation(); return ( -
+
{events.map((event, index) => event.type === 'processor.log' ? ( - - - {String(event.data.level)} - +
+ + {t('agents.eventProcessor.logsTab')} + {String(event.data.level)} +
+

{String(event.data.text)} - - - +

+
) : ( +
{t( @@ -79,6 +80,7 @@ export default function PluginProcessorTrace({ } value={event.data} /> +
), )}
diff --git a/web/src/app/home/agents/components/ProcessorRunList.tsx b/web/src/app/home/agents/components/ProcessorRunList.tsx index 0963943a9..49cef0d52 100644 --- a/web/src/app/home/agents/components/ProcessorRunList.tsx +++ b/web/src/app/home/agents/components/ProcessorRunList.tsx @@ -15,18 +15,20 @@ export default function ProcessorRunList({ selectedId, onSelect, footer, + className, }: { runs: ProcessorRun[]; selectedId?: string; onSelect: (run: ProcessorRun) => void; footer?: ReactNode; + className?: string; }) { const { t } = useTranslation(); return (
{runs.map((run) => { const duration = processorRunDuration(run); @@ -41,7 +43,7 @@ export default function ProcessorRunList({ onClick={() => onSelect(run)} aria-label={`${label} ${event} ${new Date(run.created_at * 1000).toLocaleString()}`} title={event} - className="flex h-auto min-h-12 w-full justify-start gap-3 rounded-none border-b px-3 py-2 text-left text-sm font-normal last:border-b-0 aria-pressed:bg-accent" + className="flex h-auto min-h-12 w-full justify-start gap-3 rounded-none border-b border-l-2 border-l-transparent p-4 text-left text-sm font-normal last:border-b-0 aria-pressed:border-l-primary aria-pressed:bg-primary/5" > {label} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 11ef75f52..b915c8c75 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -655,6 +655,12 @@ function NavItems({ if (item.extensionType === 'skill') { return `/home/skills?id=${encodeURIComponent(item.id)}`; } + const processorTab = searchParams.get('tab'); + if (hasDetailPages && (routePrefix === '/home/agents' || routePrefix === '/home/pipelines') && + (pathname === '/home/agents' || pathname === '/home/pipelines') && + (processorTab === 'logs' || processorTab === 'config')) { + return `${routePrefix}?id=${encodeURIComponent(item.id)}&tab=${processorTab}`; + } return hasDetailPages ? `${routePrefix}?id=${encodeURIComponent(item.id)}` : routePrefix; diff --git a/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx b/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx index 3c608fa40..83978d8a6 100644 --- a/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx +++ b/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx @@ -1,4 +1,5 @@ -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { BarChart3, Bug, Info, Settings } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -56,6 +57,7 @@ interface ProcessorDetailWorkbenchProps { debugDisconnectedLabel?: string; unsavedLabel?: string; monitoring?: ProcessorMonitoringView; + view?: 'workbench' | 'monitoring'; onViewChange?: (view: 'workbench' | 'monitoring') => void; } @@ -83,11 +85,11 @@ export default function ProcessorDetailWorkbench({ debugDisconnectedLabel, unsavedLabel, monitoring, + view, onViewChange, }: ProcessorDetailWorkbenchProps) { - const [activeView, setActiveView] = useState<'workbench' | 'monitoring'>( - 'workbench', - ); + const [searchParams, setSearchParams] = useSearchParams(); + const activeView = searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench'; const hasDebug = Boolean(debugTitle && debugContent); const configPanel = ( @@ -143,10 +145,14 @@ export default function ProcessorDetailWorkbench({ return ( { const view = value as 'workbench' | 'monitoring'; - setActiveView(view); + setSearchParams((current) => { + const next = new URLSearchParams(current); + next.set('tab', view === 'monitoring' ? 'logs' : 'config'); + return next; + }, { preventScrollReset: true }); onViewChange?.(view); }} className="flex h-full min-h-0 min-w-0 flex-col gap-0" @@ -226,7 +232,7 @@ export default function ProcessorDetailWorkbench({ )}
- {canSave && activeView === 'workbench' && ( + {canSave && (view ?? activeView) === 'workbench' && (
diff --git a/web/src/app/home/monitoring/components/TokenMonitoring.tsx b/web/src/app/home/monitoring/components/TokenMonitoring.tsx index 4871d44f9..9f549785c 100644 --- a/web/src/app/home/monitoring/components/TokenMonitoring.tsx +++ b/web/src/app/home/monitoring/components/TokenMonitoring.tsx @@ -168,7 +168,7 @@ export default function TokenMonitoring({ })); }, [stats]); - if (loading) { + if (loading && !stats) { return ; } diff --git a/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx b/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx index 8d4b36793..26fe3a6ae 100644 --- a/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx +++ b/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx @@ -74,7 +74,7 @@ function TrafficChartContent({ traffic, loading }: TrafficChartProps) { [traffic], ); - if (loading) { + if (loading && !traffic) { return (
@@ -162,6 +162,7 @@ function TrafficChartContent({ traffic, loading }: TrafficChartProps) { itemStyle={{ padding: '4px 0' }} /> (null); // Memoize filter parameters to prevent unnecessary re-renders @@ -58,8 +58,6 @@ export function useExecutions(params: { const isCurrent = () => requestId === requestIdRef.current && getCurrentWorkspaceSnapshot()?.workspace.uuid === workspaceUuid; - setRequestScope(scope); - setResult(null); setLoading(true); setError(null); @@ -88,8 +86,11 @@ export function useExecutions(params: { if (!isCurrent()) return; setResult(response); + setRequestScope(scope); } catch (err) { if (!isCurrent()) return; + setRequestScope(scope); + setResult((current) => requestScope === scope ? current : null); setError(err as Error); console.error('Failed to fetch executions:', err); } finally { @@ -107,6 +108,7 @@ export function useExecutions(params: { params.limit, params.offset, scope, + requestScope, workspaceUuid, ]); diff --git a/web/src/app/home/monitoring/hooks/useMonitoringData.ts b/web/src/app/home/monitoring/hooks/useMonitoringData.ts index 5688a1c8c..3a095a8af 100644 --- a/web/src/app/home/monitoring/hooks/useMonitoringData.ts +++ b/web/src/app/home/monitoring/hooks/useMonitoringData.ts @@ -42,8 +42,6 @@ export function useMonitoringData(filterState: FilterState) { const isCurrent = () => requestId === requestIdRef.current && getCurrentWorkspaceSnapshot()?.workspace.uuid === workspaceUuid; - setRequestScope(scope); - setData(null); setLoading(true); setError(null); @@ -387,8 +385,11 @@ export function useMonitoringData(filterState: FilterState) { ].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); setData(transformedData); + setRequestScope(scope); } catch (err) { if (!isCurrent()) return; + setRequestScope(scope); + setData((current) => requestScope === scope ? current : null); setError(err as Error); console.error('Failed to fetch monitoring data:', err); } finally { @@ -402,6 +403,7 @@ export function useMonitoringData(filterState: FilterState) { filterState.selectedBots, filterState.selectedPipelines, scope, + requestScope, workspaceUuid, ]); diff --git a/web/src/app/home/monitoring/page.tsx b/web/src/app/home/monitoring/page.tsx index e63cdc149..1a2eb0dc9 100644 --- a/web/src/app/home/monitoring/page.tsx +++ b/web/src/app/home/monitoring/page.tsx @@ -62,7 +62,7 @@ function MonitoringPageContent() { } = useMonitoringFilters(); const executionMode = filterState.mode ?? 'all'; const executionStatus = filterState.statusGroup ?? 'all'; - const { data, loading, error, refetch } = useMonitoringData({ + const { data, loading: monitoringRefreshing, error, refetch } = useMonitoringData({ ...filterState, mode: executionMode, statusGroup: executionStatus, @@ -74,7 +74,7 @@ function MonitoringPageContent() { const { result: executionResult, - loading: executionLoading, + loading: executionRefreshing, error: executionError, refetch: refetchExecutions, } = useExecutions({ @@ -120,7 +120,7 @@ function MonitoringPageContent() { const { feedback: feedbackList, stats: feedbackStats, - loading: feedbackLoading, + loading: feedbackRefreshing, } = useFeedbackData({ mode: executionMode, statusGroup: executionStatus, @@ -137,6 +137,10 @@ function MonitoringPageContent() { limit: 50, }); + const loading = monitoringRefreshing && !data; + const executionLoading = executionRefreshing && !executionResult; + const feedbackLoading = feedbackRefreshing && !feedbackStats; + const refreshing = monitoringRefreshing || executionRefreshing || feedbackRefreshing; const countdown = useRef(0); const intervalRef = useRef(0); const [remainingSeconds, setRemainingSeconds] = useState(0); @@ -157,8 +161,8 @@ function MonitoringPageContent() { }); const refreshState = useRef({ handleRefresh, busy: false }); useEffect(() => { - refreshState.current = { handleRefresh, busy: loading || executionLoading || feedbackLoading }; - }, [handleRefresh, loading, executionLoading, feedbackLoading]); + refreshState.current = { handleRefresh, busy: refreshing }; + }, [handleRefresh, refreshing]); useEffect(() => { intervalRef.current = refreshInterval; countdown.current = refreshInterval; @@ -228,10 +232,10 @@ function MonitoringPageContent() { variant="ghost" size="sm" onClick={handleRefresh} - disabled={loading || executionLoading || feedbackLoading} + disabled={refreshing} className="gap-2 rounded-none px-3" > - + {t('monitoring.refreshData')} {refreshInterval > 0 && {remainingSeconds}s} diff --git a/web/src/app/home/pipelines/PipelineDetailContent.tsx b/web/src/app/home/pipelines/PipelineDetailContent.tsx index 1066342df..3bfe01b7b 100644 --- a/web/src/app/home/pipelines/PipelineDetailContent.tsx +++ b/web/src/app/home/pipelines/PipelineDetailContent.tsx @@ -1,7 +1,7 @@ import EntityLoadState from '@/components/EntityLoadState'; import { isCurrentPipelineConfig } from './pipeline-config-safety'; import { useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; @@ -51,10 +51,8 @@ export default function PipelineDetailContent({ return () => setDetailEntityName(null); }, [id, isCreateMode, pipelines, setDetailEntityName, t]); - const [activeView, setActiveView] = useState<'workbench' | 'monitoring'>( - 'workbench', - ); - useEffect(() => setActiveView('workbench'), [id]); + const [searchParams] = useSearchParams(); + const activeView = searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench'; const [isWebSocketConnected, setIsWebSocketConnected] = useState(false); const [formDirty, setFormDirty] = useState(false); const [formSaving, setFormSaving] = useState(false); @@ -187,7 +185,6 @@ export default function PipelineDetailContent({ <>