migrations_app.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import React, { useState } from 'react';
  2. import { createRoot } from 'react-dom/client';
  3. import { Button, Space, Alert, Spin, Typography, Table } from 'antd';
  4. import axios from 'axios';
  5. import dayjs from 'dayjs';
  6. import {
  7. QueryClient,
  8. QueryClientProvider,
  9. useQuery,
  10. } from '@tanstack/react-query';
  11. const { Title } = Typography;
  12. // 创建QueryClient实例
  13. const queryClient = new QueryClient();
  14. interface MigrationResponse {
  15. success: boolean;
  16. error?: string;
  17. failedResult?: any;
  18. }
  19. interface MigrationHistory {
  20. id: string;
  21. name: string;
  22. status: string;
  23. timestamp: string;
  24. batch: string;
  25. }
  26. const MigrationsApp: React.FC = () => {
  27. const [loading, setLoading] = useState(false);
  28. const [migrationResult, setMigrationResult] = useState<MigrationResponse | null>(null);
  29. const { data: historyData, isLoading: isHistoryLoading, error: historyError } = useQuery({
  30. queryKey: ['migrations-history'],
  31. queryFn: async () => {
  32. const response = await axios.get('/api/migrations/history');
  33. return response.data.history;
  34. }
  35. });
  36. const runMigrations = async () => {
  37. try {
  38. setLoading(true);
  39. setMigrationResult(null);
  40. const response = await axios.get('/api/migrations');
  41. setMigrationResult(response.data);
  42. if (response.data.success) {
  43. queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
  44. }
  45. } catch (error: any) {
  46. setMigrationResult({
  47. success: false,
  48. error: error.response?.data?.error || '数据库迁移失败',
  49. failedResult: error.response?.data?.failedResult
  50. });
  51. } finally {
  52. setLoading(false);
  53. }
  54. };
  55. const rollbackMigrations = async (all: boolean) => {
  56. try {
  57. setLoading(true);
  58. setMigrationResult(null);
  59. const response = await axios.get(`/api/migrations/rollback?all=${all}`);
  60. setMigrationResult(response.data);
  61. if (response.data.success) {
  62. queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
  63. }
  64. } catch (error: any) {
  65. setMigrationResult({
  66. success: false,
  67. error: error.response?.data?.error || '数据库回滚失败',
  68. failedResult: error.response?.data?.failedResult
  69. });
  70. } finally {
  71. setLoading(false);
  72. }
  73. };
  74. const columns = [
  75. {
  76. title: '迁移名称',
  77. dataIndex: 'name',
  78. key: 'name',
  79. sorter: (a: MigrationHistory, b: MigrationHistory) => a.name.localeCompare(b.name),
  80. },
  81. {
  82. title: '批次',
  83. dataIndex: 'batch',
  84. key: 'batch',
  85. },
  86. {
  87. title: '状态',
  88. dataIndex: 'status',
  89. key: 'status',
  90. render: (status: string) => (
  91. <span style={{ color: status === 'completed' ? 'green' : 'red' }}>
  92. {status === 'completed' ? '已完成' : '失败'}
  93. </span>
  94. )
  95. },
  96. {
  97. title: '时间',
  98. dataIndex: 'migration_time',
  99. key: 'migration_time',
  100. render: (migration_time: string) => dayjs(migration_time).format('YYYY-MM-DD HH:mm:ss')
  101. },
  102. ];
  103. return (
  104. <div className="p-4">
  105. <Title level={3}>数据库迁移管理</Title>
  106. <Space direction="vertical" size="middle" style={{ width: '100%' }}>
  107. <Space>
  108. <Button
  109. type="primary"
  110. onClick={runMigrations}
  111. loading={loading}
  112. disabled={loading}
  113. >
  114. 执行迁移
  115. </Button>
  116. <Button
  117. danger
  118. onClick={() => rollbackMigrations(false)}
  119. loading={loading}
  120. disabled={loading}
  121. >
  122. 回滚最近一次
  123. </Button>
  124. <Button
  125. danger
  126. onClick={() => rollbackMigrations(true)}
  127. loading={loading}
  128. disabled={loading}
  129. >
  130. 回滚全部
  131. </Button>
  132. </Space>
  133. <Alert
  134. message="警告"
  135. description="回滚操作将删除数据,请谨慎使用"
  136. type="warning"
  137. showIcon
  138. style={{ marginBottom: 16 }}
  139. />
  140. {loading && <Spin tip="迁移执行中..." />}
  141. {migrationResult && (
  142. migrationResult.success ? (
  143. <Alert
  144. message="迁移成功"
  145. type="success"
  146. showIcon
  147. />
  148. ) : (
  149. <Alert
  150. message="迁移失败"
  151. description={
  152. <>
  153. <p>{migrationResult.error}</p>
  154. {migrationResult.failedResult && (
  155. <pre style={{ marginTop: 10 }}>
  156. {JSON.stringify(migrationResult.failedResult, null, 2)}
  157. </pre>
  158. )}
  159. </>
  160. }
  161. type="error"
  162. showIcon
  163. />
  164. )
  165. )}
  166. <Title level={4}>迁移历史记录</Title>
  167. {isHistoryLoading ? (
  168. <Spin tip="加载历史记录中..." />
  169. ) : historyError ? (
  170. <Alert
  171. message="加载历史记录失败"
  172. description={historyError.message}
  173. type="error"
  174. showIcon
  175. />
  176. ) : (
  177. <Table
  178. columns={columns}
  179. dataSource={historyData}
  180. rowKey="id"
  181. pagination={{
  182. pageSize: 10,
  183. showSizeChanger: true,
  184. pageSizeOptions: ['10', '20', '50', '100'],
  185. showTotal: (total) => `共 ${total} 条记录`,
  186. }}
  187. bordered
  188. className="migration-history-table"
  189. style={{ marginTop: 16 }}
  190. />
  191. )}
  192. </Space>
  193. </div>
  194. );
  195. };
  196. // 渲染应用
  197. const root = createRoot(document.getElementById('root') as HTMLElement);
  198. root.render(
  199. <QueryClientProvider client={queryClient}>
  200. <MigrationsApp />
  201. </QueryClientProvider>
  202. );