Files
FlowStack/frontend/src/App.tsx
T
Logan.Li 6e97b55015 feat: SPA 真实路由(/release、/pipelines/:id 等直达)
- 路由映射:/pipelines 列表、/pipelines/:id 编辑器、/release 批量发布、
  /records 发布记录、/gitops、/environments、/credentials、/build-nodes、
  /tools、/jenkins-import、/webhooks、/plugins、/settings
- 侧边栏/助手跳转同步更新 URL;直接输入/刷新/分享链接恢复对应页面;
  未知路径回 /pipelines
2026-10-27 09:24:24 +08:00

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>
);
}