DockerControl.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import api from '@/lib/api';
  4. import { Play, Square, RotateCw, RefreshCw, FileText, AlertCircle } from 'lucide-react';
  5. interface ServerConfig {
  6. host: string;
  7. port: number;
  8. username: string;
  9. password?: string;
  10. key_file?: string;
  11. project_path: string;
  12. }
  13. interface ContainerStatus {
  14. name: string;
  15. status: string;
  16. image: string;
  17. }
  18. export default function DockerControl({
  19. serverConfig,
  20. serverId,
  21. projectPath
  22. }: {
  23. serverConfig: ServerConfig;
  24. serverId?: string;
  25. projectPath?: string;
  26. }) {
  27. const [containers, setContainers] = useState<Record<string, ContainerStatus>>({});
  28. const [loading, setLoading] = useState(false);
  29. const [error, setError] = useState<string | null>(null);
  30. const fetchStatus = async () => {
  31. setLoading(true);
  32. setError(null);
  33. try {
  34. const url = serverId ? '/api/remote/server/docker/status' : '/api/remote/docker/status';
  35. const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
  36. const response = await api.post(url, payload);
  37. if (response.data.code === 0) {
  38. setContainers(response.data.data.containers || {});
  39. } else {
  40. setError(response.data.message || '获取状态失败');
  41. }
  42. } catch (err: any) {
  43. setError(err.response?.data?.message || err.message || '请求失败');
  44. } finally {
  45. setLoading(false);
  46. }
  47. };
  48. useEffect(() => {
  49. fetchStatus();
  50. }, [serverId, projectPath]);
  51. const handleDockerAction = async (action: 'start' | 'stop' | 'restart', containerName: string) => {
  52. setLoading(true);
  53. setError(null);
  54. try {
  55. const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
  56. const payload = serverId
  57. ? { server_id: serverId, container_name: containerName, project_path: projectPath }
  58. : { ...serverConfig, container_name: containerName, project_path: projectPath };
  59. const response = await api.post(url, payload);
  60. if (response.data.code === 0) {
  61. alert('操作成功');
  62. await fetchStatus();
  63. } else {
  64. setError(response.data.message || '操作失败');
  65. }
  66. } catch (err: any) {
  67. setError(err.response?.data?.message || err.message || '操作失败');
  68. } finally {
  69. setLoading(false);
  70. }
  71. };
  72. const handleComposeAction = async (action: 'up' | 'down') => {
  73. setLoading(true);
  74. setError(null);
  75. try {
  76. const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
  77. const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
  78. const response = await api.post(url, payload);
  79. if (response.data.code === 0) {
  80. alert('操作成功');
  81. await fetchStatus();
  82. } else {
  83. setError(response.data.message || '操作失败');
  84. }
  85. } catch (err: any) {
  86. setError(err.response?.data?.message || err.message || '操作失败');
  87. } finally {
  88. setLoading(false);
  89. }
  90. };
  91. return (
  92. <div className="space-y-6">
  93. {/* 操作栏 */}
  94. <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
  95. <div className="flex items-center gap-2">
  96. <h3 className="text-lg font-semibold text-slate-800">Docker 容器管理</h3>
  97. <button
  98. onClick={fetchStatus}
  99. disabled={loading}
  100. className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors disabled:opacity-50"
  101. title="刷新状态"
  102. >
  103. <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
  104. </button>
  105. </div>
  106. <div className="grid grid-cols-2 sm:flex gap-2">
  107. <button
  108. onClick={() => handleComposeAction('up')}
  109. disabled={loading}
  110. className="px-3 sm:px-4 py-2 bg-green-600 text-white text-sm sm:text-base rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
  111. >
  112. <Play size={18} />
  113. <span className="whitespace-nowrap">全部启动</span>
  114. </button>
  115. <button
  116. onClick={() => handleComposeAction('down')}
  117. disabled={loading}
  118. className="px-3 sm:px-4 py-2 bg-red-600 text-white text-sm sm:text-base rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
  119. >
  120. <Square size={18} />
  121. <span className="whitespace-nowrap">全部停止</span>
  122. </button>
  123. </div>
  124. </div>
  125. {error && (
  126. <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md flex items-center gap-2">
  127. <AlertCircle size={18} />
  128. {error}
  129. </div>
  130. )}
  131. {/* 容器列表 */}
  132. <div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
  133. {/* 桌面端表格视图 */}
  134. <div className="hidden md:block overflow-x-auto">
  135. <table className="w-full">
  136. <thead className="bg-slate-50 border-b border-slate-200">
  137. <tr>
  138. <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">容器名称</th>
  139. <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">状态</th>
  140. <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">镜像</th>
  141. <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">操作</th>
  142. </tr>
  143. </thead>
  144. <tbody className="divide-y divide-slate-200">
  145. {Object.keys(containers).length === 0 ? (
  146. <tr>
  147. <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
  148. {loading ? '加载中...' : '暂无容器'}
  149. </td>
  150. </tr>
  151. ) : (
  152. Object.entries(containers).map(([name, container]) => (
  153. <tr key={name} className="hover:bg-slate-50">
  154. <td className="px-4 py-3 text-sm text-slate-800 font-medium">{name}</td>
  155. <td className="px-4 py-3 text-sm">
  156. <span
  157. className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${
  158. container.status.includes('Up')
  159. ? 'bg-green-100 text-green-800'
  160. : 'bg-red-100 text-red-800'
  161. }`}
  162. >
  163. {container.status}
  164. </span>
  165. </td>
  166. <td className="px-4 py-3 text-sm text-slate-600">{container.image}</td>
  167. <td className="px-4 py-3 text-right">
  168. <div className="flex items-center justify-end gap-2">
  169. <button
  170. onClick={() => handleDockerAction('start', name)}
  171. disabled={loading || container.status.includes('Up')}
  172. className="p-1.5 text-green-600 hover:bg-green-50 rounded transition-colors disabled:opacity-50"
  173. title="启动"
  174. >
  175. <Play size={16} />
  176. </button>
  177. <button
  178. onClick={() => handleDockerAction('stop', name)}
  179. disabled={loading || !container.status.includes('Up')}
  180. className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-50"
  181. title="停止"
  182. >
  183. <Square size={16} />
  184. </button>
  185. <button
  186. onClick={() => handleDockerAction('restart', name)}
  187. disabled={loading}
  188. className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors disabled:opacity-50"
  189. title="重启"
  190. >
  191. <RotateCw size={16} />
  192. </button>
  193. </div>
  194. </td>
  195. </tr>
  196. ))
  197. )}
  198. </tbody>
  199. </table>
  200. </div>
  201. {/* 移动端卡片视图 */}
  202. <div className="md:hidden divide-y divide-slate-200">
  203. {Object.keys(containers).length === 0 ? (
  204. <div className="px-4 py-8 text-center text-slate-500">
  205. {loading ? '加载中...' : '暂无容器'}
  206. </div>
  207. ) : (
  208. Object.entries(containers).map(([name, container]) => (
  209. <div key={name} className="p-4 space-y-3">
  210. <div className="flex items-center justify-between">
  211. <span className="text-sm font-semibold text-slate-800">{name}</span>
  212. <span
  213. className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${
  214. container.status.includes('Up')
  215. ? 'bg-green-100 text-green-800'
  216. : 'bg-red-100 text-red-800'
  217. }`}
  218. >
  219. {container.status}
  220. </span>
  221. </div>
  222. <div className="text-xs text-slate-500 truncate">
  223. <span className="font-medium text-slate-600 mr-1">镜像:</span>
  224. {container.image}
  225. </div>
  226. <div className="flex items-center justify-between pt-2 border-t border-slate-100">
  227. <div className="flex gap-4">
  228. <button
  229. onClick={() => handleDockerAction('start', name)}
  230. disabled={loading || container.status.includes('Up')}
  231. className="flex flex-col items-center gap-1 text-green-600 disabled:opacity-30"
  232. >
  233. <Play size={18} />
  234. <span className="text-[10px]">启动</span>
  235. </button>
  236. <button
  237. onClick={() => handleDockerAction('stop', name)}
  238. disabled={loading || !container.status.includes('Up')}
  239. className="flex flex-col items-center gap-1 text-red-600 disabled:opacity-30"
  240. >
  241. <Square size={18} />
  242. <span className="text-[10px]">停止</span>
  243. </button>
  244. <button
  245. onClick={() => handleDockerAction('restart', name)}
  246. disabled={loading}
  247. className="flex flex-col items-center gap-1 text-blue-600 disabled:opacity-30"
  248. >
  249. <RotateCw size={18} />
  250. <span className="text-[10px]">重启</span>
  251. </button>
  252. </div>
  253. </div>
  254. </div>
  255. ))
  256. )}
  257. </div>
  258. </div>
  259. </div>
  260. );
  261. }