-
- {
-
-
+
+
+
+
+ {activeSection === 'runner' && (
+
+ {renderRunnerStatus()}
+ {runnerSelectorStage
+ ? renderDynamicStage(runnerSelectorStage)
+ : !runnerConfigSchema && (
+
+
+
+ {t('agents.runnerSettings')}
+
+
+ {t('agents.noRunnerMetadata')}
+
+
+
+ )}
+
+ )}
+
+ {activeSection === 'runner_config' && (
+
+ {activeRunnerStage ? (
+ renderDynamicStage(activeRunnerStage)
+ ) : (
+
- {t('agents.basicInfo')}
+ {t('agents.runnerSettings')}
- {t('agents.basicInfoDescription')}
+ {t('agents.noRunnerMetadata')}
-
-
- (
-
-
- {t('common.name')}
- *
-
-
-
-
-
-
- )}
- />
- (
-
- {t('common.icon')}
-
-
-
-
-
- )}
- />
-
+
+ )}
+
+ )}
+ {activeSection === 'events' && (
+
+
+ {t('agents.bindableEvents')}
+
+ {t('agents.bindableEventsDescription')}
+
+
+
+ (
+
+ {t('agents.supportedEvents')}
+
+
+
+
+ {t('agents.supportedEventsDescription')}
+
+
+
+ )}
+ />
+
+
+ )}
+
+ {activeSection === 'basic' && (
+
+
+
+ {t('agents.basicInfo')}
+
+ {t('agents.basicInfoDescription')}
+
+
+
+
(
-
- {t('common.description')}
+
+
+ {t('common.name')}
+ *
+
@@ -497,122 +618,98 @@ export default function AgentFormComponent({
)}
/>
-
(
-
-
-
-
- {t('agents.enabled')}
-
-
- {t('agents.enabledDescription')}
-
-
+
+ {t('common.icon')}
-
+
)}
/>
-
-
+
-
-
-
- {t('agents.dangerZone')}
-
-
- {t('agents.dangerZoneDescription')}
-
-
-
-
-
-
- {t('agents.deleteAgentAction')}
-
-
- {t('agents.deleteAgentHint')}
-
-
-
-
-
-
-
- }
-
- {
-
- {renderRunnerStatus()}
- {runnerConfigSchema?.stages.map((stage) =>
- renderDynamicStage(stage),
- )}
- {!runnerConfigSchema && (
-
-
- {t('agents.runnerSettings')}
-
- {t('agents.noRunnerMetadata')}
-
-
-
- )}
-
- }
-
- {
-
-
- {t('agents.bindableEvents')}
-
- {t('agents.bindableEventsDescription')}
-
-
-
(
-
- {t('agents.supportedEvents')}
-
+ {t('common.description')}
-
+
+
+
+ )}
+ />
+
+ (
+
+
+
+
+ {t('agents.enabled')}
+
+
+ {t('agents.enabledDescription')}
+
+
+
+
-
- {t('agents.supportedEventsDescription')}
-
-
)}
/>
- }
-
+
+
+
+
+ {t('agents.dangerZone')}
+
+
+ {t('agents.dangerZoneDescription')}
+
+
+
+
+
+
+ {t('agents.deleteAgentAction')}
+
+
+ {t('agents.deleteAgentHint')}
+
+
+
+
+
+
+
+ )}
diff --git a/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx b/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx
new file mode 100644
index 000000000..674bc90d0
--- /dev/null
+++ b/web/src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx
@@ -0,0 +1,149 @@
+import { ReactNode, useState } from 'react';
+import { BarChart3, Bug, Settings } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+interface ProcessorMonitoringView {
+ label: string;
+ content: ReactNode;
+}
+
+interface ProcessorDetailWorkbenchProps {
+ title: string;
+ saveLabel: string;
+ saveFormId: string;
+ canSave: boolean;
+ isDirty: boolean;
+ isSaving: boolean;
+ configTitle: string;
+ configContent: ReactNode;
+ debugTitle?: string;
+ debugContent?: ReactNode;
+ debugConnected?: boolean;
+ debugConnectedLabel?: string;
+ debugDisconnectedLabel?: string;
+ unsavedLabel?: string;
+ monitoring?: ProcessorMonitoringView;
+}
+
+export default function ProcessorDetailWorkbench({
+ title,
+ saveLabel,
+ saveFormId,
+ canSave,
+ isDirty,
+ isSaving,
+ configTitle,
+ configContent,
+ debugTitle,
+ debugContent,
+ debugConnected,
+ debugConnectedLabel,
+ debugDisconnectedLabel,
+ unsavedLabel,
+ monitoring,
+}: ProcessorDetailWorkbenchProps) {
+ const [activeView, setActiveView] = useState<'workbench' | 'monitoring'>(
+ 'workbench',
+ );
+ const hasDebug = Boolean(debugTitle && debugContent);
+
+ return (
+
+
+
{title}
+
+ {monitoring && (
+
+ )}
+ {canSave && activeView === 'workbench' && (
+
+ )}
+
+
+
+ {activeView === 'monitoring' && monitoring ? (
+
+ ) : (
+
+
+ {hasDebug && (
+
+
+
+
+ {debugTitle}
+
+ {debugConnected !== undefined && (
+
+
+ {debugConnected
+ ? debugConnectedLabel
+ : debugDisconnectedLabel}
+
+ )}
+
+
+ {debugContent}
+
+
+ )}
+
+
+
+
+ {configTitle}
+ {isDirty && (
+
+
+ {unsavedLabel}
+
+ )}
+
+
+ {configContent}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/web/src/app/home/pipelines/PipelineDetailContent.tsx b/web/src/app/home/pipelines/PipelineDetailContent.tsx
index 48fe1fc3a..769703190 100644
--- a/web/src/app/home/pipelines/PipelineDetailContent.tsx
+++ b/web/src/app/home/pipelines/PipelineDetailContent.tsx
@@ -1,13 +1,12 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import PipelineFormComponent from '@/app/home/pipelines/components/pipeline-form/PipelineFormComponent';
import DebugDialog from '@/app/home/pipelines/components/debug-dialog/DebugDialog';
import PipelineMonitoringTab from '@/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab';
+import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import { useTranslation } from 'react-i18next';
-import { Settings, Bug, BarChart3 } from 'lucide-react';
import { useCurrentWorkspace } from '@/app/infra/http';
export default function PipelineDetailContent({
@@ -40,7 +39,6 @@ export default function PipelineDetailContent({
return () => setDetailEntityName(null);
}, [id, isCreateMode, pipelines, setDetailEntityName, t]);
- const [activeTab, setActiveTab] = useState('config');
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false);
@@ -96,103 +94,62 @@ export default function PipelineDetailContent({
// ==================== Edit Mode ====================
return (
-
- {/* Sticky Header: title + save button */}
-
-
{t('pipelines.editPipeline')}
- {canManage && (
-
- )}
-
-
- {/* Horizontal Tabs */}
-
-
-
-
- {t('pipelines.configuration')}
-
- {canOperate && (
-
-
- {t('pipelines.debugChat')}
- {activeTab === 'debug' && (
-
+ navigate(routeBase)}
+ onDirtyChange={setFormDirty}
+ onSavingChange={setFormSaving}
+ />
+
+ }
+ debugTitle={canOperate ? t('pipelines.debugChat') : undefined}
+ debugConnected={canOperate ? isWebSocketConnected : undefined}
+ debugConnectedLabel={t('pipelines.debugDialog.connected')}
+ debugDisconnectedLabel={t('pipelines.debugDialog.disconnected')}
+ debugContent={
+ canOperate ? (
+
+ ) : undefined
+ }
+ unsavedLabel={t('pipelines.unsavedChanges')}
+ monitoring={
+ canViewMonitoring
+ ? {
+ label: t('pipelines.monitoring.title'),
+ content: (
+ {
+ navigate('/home/monitoring');
+ }}
/>
- )}
-
- )}
- {canViewMonitoring && (
-
-
- {t('pipelines.monitoring.title')}
-
- )}
-
-
- {/* Tab: Configuration */}
-
-
-
-
- {/* Tab: Debug */}
- {canOperate && (
-
-
-
- )}
-
- {/* Tab: Monitoring */}
- {canViewMonitoring && (
-
- {
- navigate('/home/monitoring');
- }}
- />
-
- )}
-
-
+ ),
+ }
+ : undefined
+ }
+ />
);
}
diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
index ff3d7b39d..cb56bb12e 100644
--- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
+++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
@@ -46,6 +46,7 @@ interface DebugDialogProps {
open: boolean;
pipelineId: string;
isEmbedded?: boolean;
+ compact?: boolean;
onConnectionStatusChange?: (isConnected: boolean) => void;
}
@@ -115,6 +116,7 @@ export default function DebugDialog({
open,
pipelineId,
isEmbedded = false,
+ compact = false,
onConnectionStatusChange,
}: DebugDialogProps) {
const { t } = useTranslation();
@@ -805,7 +807,12 @@ export default function DebugDialog({
const renderContent = () => (
-
+