- 路由映射:/pipelines 列表、/pipelines/:id 编辑器、/release 批量发布、 /records 发布记录、/gitops、/environments、/credentials、/build-nodes、 /tools、/jenkins-import、/webhooks、/plugins、/settings - 侧边栏/助手跳转同步更新 URL;直接输入/刷新/分享链接恢复对应页面; 未知路径回 /pipelines
241 lines
8.9 KiB
TypeScript
241 lines
8.9 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import { Toaster } from 'sonner';
|
|
import { useLocation, useNavigate } from 'react-router';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import Sidebar from '@/components/Sidebar';
|
|
import TopBar from '@/components/TopBar';
|
|
import PipelineFlowEditor from '@/components/flow/PipelineFlowEditor';
|
|
import BasicInfoPanel from '@/components/BasicInfoPanel';
|
|
import TriggerPanel from '@/components/TriggerPanel';
|
|
import VariablesPanel from '@/components/VariablesPanel';
|
|
import ReleasePanel from '@/components/ReleasePanel';
|
|
import PipelineList from '@/pages/PipelineList';
|
|
import EnvironmentManager from '@/pages/EnvironmentManager';
|
|
import CredentialManager from '@/pages/CredentialManager';
|
|
import BuildNodeManager from '@/pages/BuildNodeManager';
|
|
import ToolConfig from '@/pages/ToolConfig';
|
|
import JenkinsImport from '@/pages/JenkinsImport';
|
|
import BatchRelease from '@/pages/BatchRelease';
|
|
import ReleaseRecords from '@/pages/ReleaseRecords';
|
|
import WebhookConfig from '@/pages/WebhookConfig';
|
|
import GitOpsDeploy from '@/pages/GitOpsDeploy';
|
|
import PluginManager from '@/pages/PluginManager';
|
|
import SystemSettings from '@/pages/SystemSettings';
|
|
import AiAssistant from '@/components/ai/AiAssistant';
|
|
import { apiClient } from '@/lib/api';
|
|
|
|
export type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'webhooks' | 'plugins' | 'settings';
|
|
export type EditorTab = '基本信息' | '流程配置' | '触发设置' | '变量和缓存' | '发布';
|
|
|
|
// 路由 ↔ 模块映射(用户可直接访问 /release、/pipelines/1 等)
|
|
const ROUTE_TO_MODULE: Record<string, ModuleType> = {
|
|
'/': 'pipelines',
|
|
'/pipelines': 'pipelines',
|
|
'/gitops': 'gitops',
|
|
'/release': 'batch-release',
|
|
'/records': 'release-records',
|
|
'/environments': 'environments',
|
|
'/credentials': 'credentials',
|
|
'/build-nodes': 'build-nodes',
|
|
'/tools': 'tools',
|
|
'/jenkins-import': 'jenkins-import',
|
|
'/webhooks': 'webhooks',
|
|
'/plugins': 'plugins',
|
|
'/settings': 'settings',
|
|
};
|
|
|
|
const MODULE_TO_ROUTE: Record<ModuleType, string> = {
|
|
pipelines: '/pipelines',
|
|
gitops: '/gitops',
|
|
'batch-release': '/release',
|
|
'release-records': '/records',
|
|
environments: '/environments',
|
|
credentials: '/credentials',
|
|
'build-nodes': '/build-nodes',
|
|
tools: '/tools',
|
|
'jenkins-import': '/jenkins-import',
|
|
webhooks: '/webhooks',
|
|
plugins: '/plugins',
|
|
settings: '/settings',
|
|
};
|
|
|
|
export default function App() {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
// Module navigation
|
|
const [activeModule, setActiveModule] = useState<ModuleType>('pipelines');
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
// Pipeline list/editor view toggle
|
|
const [pipelineView, setPipelineView] = useState<'list' | 'editor'>('list');
|
|
const [currentPipelineId, setCurrentPipelineId] = useState<number>(1);
|
|
const [activeTab, setActiveTab] = useState<EditorTab>('流程配置');
|
|
const [saveTick, setSaveTick] = useState(0);
|
|
const [runTick, setRunTick] = useState(0);
|
|
|
|
// URL → 页面状态(直接输入/刷新/分享链接时恢复)
|
|
useEffect(() => {
|
|
const path = location.pathname.replace(/\/+$/, '') || '/';
|
|
const editorMatch = path.match(/^\/pipelines\/(\d+)/);
|
|
if (editorMatch) {
|
|
setActiveModule('pipelines');
|
|
setCurrentPipelineId(Number(editorMatch[1]) || 1);
|
|
setPipelineView('editor');
|
|
return;
|
|
}
|
|
const mod = ROUTE_TO_MODULE[path];
|
|
if (mod) {
|
|
setActiveModule(mod);
|
|
if (mod !== 'pipelines') {
|
|
setPipelineView('list');
|
|
} else if (path === '/pipelines' || path === '/') {
|
|
setPipelineView('list');
|
|
}
|
|
return;
|
|
}
|
|
// 未知路径回首页
|
|
navigate('/pipelines', { replace: true });
|
|
}, [location.pathname, navigate]);
|
|
|
|
const goToModule = useCallback((m: ModuleType) => {
|
|
setActiveModule(m);
|
|
navigate(MODULE_TO_ROUTE[m]);
|
|
}, [navigate]);
|
|
|
|
const { data: pipelines = [] } = useQuery({
|
|
queryKey: ['pipelines'],
|
|
queryFn: apiClient.listPipelines,
|
|
});
|
|
|
|
// 当前流水线最近一次运行状态(TopBar 徽章)
|
|
const { data: currentRuns = [] } = useQuery({
|
|
queryKey: ['runs', { pipelineId: currentPipelineId }],
|
|
queryFn: () => apiClient.listRuns({ pipelineId: currentPipelineId }),
|
|
refetchInterval: (query) =>
|
|
query.state.data?.[0]?.status === 'running' || query.state.data?.[0]?.status === 'pending' ? 2000 : false,
|
|
});
|
|
|
|
const currentPipeline = pipelines.find((p: any) => p.id === currentPipelineId);
|
|
const latestStatus = currentRuns[0]?.status as string | undefined;
|
|
const isPipelineRunning = latestStatus === 'running' || latestStatus === 'pending';
|
|
|
|
const handleSelectPipeline = useCallback((id: string) => {
|
|
setCurrentPipelineId(Number(id) || 1);
|
|
setPipelineView('editor');
|
|
setActiveTab('流程配置');
|
|
navigate(`/pipelines/${Number(id) || 1}`);
|
|
}, [navigate]);
|
|
|
|
const handlePipelineSwitch = useCallback((id: number) => {
|
|
setCurrentPipelineId(id);
|
|
setActiveTab('流程配置');
|
|
navigate(`/pipelines/${id}`);
|
|
}, [navigate]);
|
|
|
|
const handleOnlySave = useCallback(() => {
|
|
if (activeTab !== '流程配置') setActiveTab('流程配置');
|
|
setSaveTick((t) => t + 1);
|
|
}, [activeTab]);
|
|
|
|
const handleSaveAndRun = useCallback(() => {
|
|
setActiveTab('流程配置');
|
|
setRunTick((t) => t + 1);
|
|
}, []);
|
|
|
|
// Render content based on active module
|
|
const renderContent = () => {
|
|
switch (activeModule) {
|
|
case 'pipelines':
|
|
if (pipelineView === 'list') {
|
|
return <PipelineList onSelectPipeline={handleSelectPipeline} />;
|
|
}
|
|
// Pipeline editor mode
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<TopBar
|
|
pipelineName={currentPipeline?.name || '...'}
|
|
pipelines={pipelines.map((p: any) => ({ id: p.id, name: p.name }))}
|
|
currentPipelineId={currentPipelineId}
|
|
onPipelineChange={handlePipelineSwitch}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
onOnlySave={handleOnlySave}
|
|
onSaveAndRun={handleSaveAndRun}
|
|
isRunning={isPipelineRunning}
|
|
executionStatus={(latestStatus as 'running' | 'success' | 'failed' | 'cancelled') || 'idle'}
|
|
onBack={() => { setPipelineView('list'); navigate('/pipelines'); }}
|
|
/>
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{activeTab === '流程配置' && (
|
|
<div className="flex-1 overflow-hidden">
|
|
<PipelineFlowEditor pipelineId={currentPipelineId} saveSignal={saveTick} runSignal={runTick} />
|
|
</div>
|
|
)}
|
|
{activeTab === '基本信息' && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<BasicInfoPanel pipelineId={currentPipelineId} />
|
|
</div>
|
|
)}
|
|
{activeTab === '触发设置' && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<TriggerPanel pipelineId={currentPipelineId} />
|
|
</div>
|
|
)}
|
|
{activeTab === '变量和缓存' && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<VariablesPanel pipelineId={currentPipelineId} />
|
|
</div>
|
|
)}
|
|
{activeTab === '发布' && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<ReleasePanel pipelineId={currentPipelineId} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
case 'environments':
|
|
return <EnvironmentManager />;
|
|
case 'credentials':
|
|
return <CredentialManager />;
|
|
case 'build-nodes':
|
|
return <BuildNodeManager />;
|
|
case 'tools':
|
|
return <ToolConfig />;
|
|
case 'jenkins-import':
|
|
return <JenkinsImport />;
|
|
case 'batch-release':
|
|
return <BatchRelease onViewRecords={() => goToModule('release-records')} />;
|
|
case 'release-records':
|
|
return <ReleaseRecords />;
|
|
case 'webhooks':
|
|
return <WebhookConfig />;
|
|
case 'gitops':
|
|
return <GitOpsDeploy />;
|
|
case 'plugins':
|
|
return <PluginManager />;
|
|
case 'settings':
|
|
return <SystemSettings />;
|
|
default:
|
|
return <PipelineList onSelectPipeline={handleSelectPipeline} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="h-screen w-screen flex overflow-hidden" style={{ background: 'var(--canvas-bg)', color: 'var(--text-primary)' }}>
|
|
<Toaster position="top-right" richColors />
|
|
<Sidebar
|
|
activeModule={activeModule}
|
|
onModuleChange={goToModule}
|
|
collapsed={sidebarCollapsed}
|
|
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
/>
|
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
{renderContent()}
|
|
</div>
|
|
<AiAssistant onNavigate={(m) => goToModule(m as ModuleType)} />
|
|
</div>
|
|
);
|
|
}
|