220 lines
5.8 KiB
TypeScript
220 lines
5.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { Button, Space, Alert, Spin, Typography, Table } from 'antd';
|
|
import axios from 'axios';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
QueryClient,
|
|
QueryClientProvider,
|
|
useQuery,
|
|
} from '@tanstack/react-query';
|
|
|
|
const { Title } = Typography;
|
|
|
|
// 创建QueryClient实例
|
|
const queryClient = new QueryClient();
|
|
|
|
interface MigrationResponse {
|
|
success: boolean;
|
|
error?: string;
|
|
failedResult?: any;
|
|
}
|
|
|
|
interface MigrationHistory {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
timestamp: string;
|
|
batch: string;
|
|
}
|
|
|
|
const MigrationsApp: React.FC = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [migrationResult, setMigrationResult] = useState<MigrationResponse | null>(null);
|
|
|
|
const { data: historyData, isLoading: isHistoryLoading, error: historyError } = useQuery({
|
|
queryKey: ['migrations-history'],
|
|
queryFn: async () => {
|
|
const response = await axios.get('/api/migrations/history');
|
|
return response.data.history;
|
|
}
|
|
});
|
|
|
|
const runMigrations = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setMigrationResult(null);
|
|
|
|
const response = await axios.get('/api/migrations');
|
|
setMigrationResult(response.data);
|
|
if (response.data.success) {
|
|
queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
|
|
}
|
|
} catch (error: any) {
|
|
setMigrationResult({
|
|
success: false,
|
|
error: error.response?.data?.error || '数据库迁移失败',
|
|
failedResult: error.response?.data?.failedResult
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const rollbackMigrations = async (all: boolean) => {
|
|
try {
|
|
setLoading(true);
|
|
setMigrationResult(null);
|
|
|
|
const response = await axios.get(`/api/migrations/rollback?all=${all}`);
|
|
setMigrationResult(response.data);
|
|
if (response.data.success) {
|
|
queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
|
|
}
|
|
} catch (error: any) {
|
|
setMigrationResult({
|
|
success: false,
|
|
error: error.response?.data?.error || '数据库回滚失败',
|
|
failedResult: error.response?.data?.failedResult
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '迁移名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
sorter: (a: MigrationHistory, b: MigrationHistory) => a.name.localeCompare(b.name),
|
|
},
|
|
{
|
|
title: '批次',
|
|
dataIndex: 'batch',
|
|
key: 'batch',
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
render: (status: string) => (
|
|
<span style={{ color: status === 'completed' ? 'green' : 'red' }}>
|
|
{status === 'completed' ? '已完成' : '失败'}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'migration_time',
|
|
key: 'migration_time',
|
|
render: (migration_time: string) => dayjs(migration_time).format('YYYY-MM-DD HH:mm:ss')
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<Title level={3}>数据库迁移管理</Title>
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
onClick={runMigrations}
|
|
loading={loading}
|
|
disabled={loading}
|
|
>
|
|
执行迁移
|
|
</Button>
|
|
<Button
|
|
danger
|
|
onClick={() => rollbackMigrations(false)}
|
|
loading={loading}
|
|
disabled={loading}
|
|
>
|
|
回滚最近一次
|
|
</Button>
|
|
<Button
|
|
danger
|
|
onClick={() => rollbackMigrations(true)}
|
|
loading={loading}
|
|
disabled={loading}
|
|
>
|
|
回滚全部
|
|
</Button>
|
|
</Space>
|
|
<Alert
|
|
message="警告"
|
|
description="回滚操作将删除数据,请谨慎使用"
|
|
type="warning"
|
|
showIcon
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
|
|
{loading && <Spin tip="迁移执行中..." />}
|
|
|
|
{migrationResult && (
|
|
migrationResult.success ? (
|
|
<Alert
|
|
message="迁移成功"
|
|
type="success"
|
|
showIcon
|
|
/>
|
|
) : (
|
|
<Alert
|
|
message="迁移失败"
|
|
description={
|
|
<>
|
|
<p>{migrationResult.error}</p>
|
|
{migrationResult.failedResult && (
|
|
<pre style={{ marginTop: 10 }}>
|
|
{JSON.stringify(migrationResult.failedResult, null, 2)}
|
|
</pre>
|
|
)}
|
|
</>
|
|
}
|
|
type="error"
|
|
showIcon
|
|
/>
|
|
)
|
|
)}
|
|
|
|
<Title level={4}>迁移历史记录</Title>
|
|
|
|
{isHistoryLoading ? (
|
|
<Spin tip="加载历史记录中..." />
|
|
) : historyError ? (
|
|
<Alert
|
|
message="加载历史记录失败"
|
|
description={historyError.message}
|
|
type="error"
|
|
showIcon
|
|
/>
|
|
) : (
|
|
<Table
|
|
columns={columns}
|
|
dataSource={historyData}
|
|
rowKey="id"
|
|
pagination={{
|
|
pageSize: 10,
|
|
showSizeChanger: true,
|
|
pageSizeOptions: ['10', '20', '50', '100'],
|
|
showTotal: (total) => `共 ${total} 条记录`,
|
|
}}
|
|
bordered
|
|
className="migration-history-table"
|
|
style={{ marginTop: 16 }}
|
|
/>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 渲染应用
|
|
const root = createRoot(document.getElementById('root') as HTMLElement);
|
|
root.render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MigrationsApp />
|
|
</QueryClientProvider>
|
|
); |