2
0

migrations_app.tsx 5.5 KB

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