ImportTasksDrawer.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import React, { useState, useEffect } from 'react';
  2. import { X, Box, Loader2, Trash2 } from 'lucide-react';
  3. import { importService, ImportTask } from '../../services/importService';
  4. import { useLanguage } from '../../contexts/LanguageContext';
  5. import { knowledgeGroupService } from '../../services/knowledgeGroupService';
  6. import { KnowledgeGroup } from '../../types';
  7. import { useToast } from '../../contexts/ToastContext';
  8. import ConfirmDialog from '../ConfirmDialog';
  9. interface ImportTasksDrawerProps {
  10. isOpen: boolean;
  11. onClose: () => void;
  12. authToken: string;
  13. }
  14. export const ImportTasksDrawer: React.FC<ImportTasksDrawerProps> = ({
  15. isOpen,
  16. onClose,
  17. authToken,
  18. }) => {
  19. const { t } = useLanguage();
  20. const { showError } = useToast();
  21. const [importTasks, setImportTasks] = useState<ImportTask[]>([]);
  22. const [groups, setGroups] = useState<KnowledgeGroup[]>([]);
  23. const [isLoading, setIsLoading] = useState(false);
  24. const [taskToDelete, setTaskToDelete] = useState<string | null>(null);
  25. const fetchData = async () => {
  26. if (!authToken) return;
  27. try {
  28. setIsLoading(true);
  29. const [tasksResult, groupsData] = await Promise.all([
  30. importService.getAll(authToken),
  31. knowledgeGroupService.getGroups()
  32. ]);
  33. setImportTasks(tasksResult.items);
  34. // Flatten the groups tree so we can easily find names by ID
  35. const flat: KnowledgeGroup[] = [];
  36. const walk = (items: KnowledgeGroup[]) => {
  37. for (const g of items) {
  38. flat.push(g);
  39. if (g.children?.length) walk(g.children);
  40. }
  41. };
  42. if (groupsData) walk(groupsData);
  43. setGroups(flat);
  44. } catch (error) {
  45. console.error('Failed to fetch data:', error);
  46. } finally {
  47. setIsLoading(false);
  48. }
  49. };
  50. const handleDelete = async (taskId: string) => {
  51. setTaskToDelete(taskId);
  52. };
  53. const confirmDelete = async () => {
  54. if (!taskToDelete) return;
  55. try {
  56. await importService.delete(authToken, taskToDelete);
  57. fetchData();
  58. } catch (error) {
  59. console.error('Failed to delete task:', error);
  60. showError(t('deleteTaskFailed'));
  61. } finally {
  62. setTaskToDelete(null);
  63. }
  64. };
  65. useEffect(() => {
  66. if (isOpen) {
  67. fetchData();
  68. }
  69. }, [isOpen, authToken]);
  70. if (!isOpen) return null;
  71. return (
  72. <div className="fixed inset-0 z-50 flex justify-end">
  73. <div className="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity" onClick={onClose} />
  74. <div className="relative w-full max-w-4xl bg-white shadow-2xl flex flex-col h-full animate-in slide-in-from-right duration-300">
  75. {/* Header */}
  76. <div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between shrink-0 bg-white">
  77. <div className="flex items-center gap-3">
  78. <div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600">
  79. <Box size={16} />
  80. </div>
  81. <h2 className="text-lg font-bold text-slate-800">{t('importTasksTitle')}</h2>
  82. </div>
  83. <div className="flex items-center gap-2">
  84. <button
  85. onClick={fetchData}
  86. className="p-2 text-slate-400 hover:text-indigo-600 hover:bg-slate-50 rounded-lg transition-all"
  87. title={t('refresh')}
  88. >
  89. <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
  90. </button>
  91. <button
  92. onClick={onClose}
  93. className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-colors"
  94. >
  95. <X size={20} />
  96. </button>
  97. </div>
  98. </div>
  99. {/* Body */}
  100. <div className="flex-1 overflow-y-auto p-6 bg-slate-50/30">
  101. {isLoading ? (
  102. <div className="p-12 flex justify-center">
  103. <Loader2 className="animate-spin text-slate-300 w-8 h-8" />
  104. </div>
  105. ) : importTasks.length === 0 ? (
  106. <div className="p-12 text-center text-slate-400 flex flex-col items-center">
  107. <Box size={48} className="mb-4 opacity-20" />
  108. <span className="text-sm font-bold uppercase tracking-widest">{t('noTasksFound')}</span>
  109. </div>
  110. ) : (
  111. <div className="bg-white rounded-2xl border border-slate-200/60 shadow-sm overflow-hidden">
  112. <div className="overflow-x-auto">
  113. <table className="w-full text-left">
  114. <thead>
  115. <tr className="border-b border-slate-200/60 bg-slate-50/50 text-[10px] font-black uppercase tracking-widest text-slate-500">
  116. <th className="px-6 py-4">{t('sourcePath')}</th>
  117. <th className="px-6 py-4">{t('targetGroup')}</th>
  118. <th className="px-6 py-4">{t('status')}</th>
  119. <th className="px-6 py-4">{t('scheduledAt')}</th>
  120. <th className="px-6 py-4">{t('createdAt')}</th>
  121. <th className="px-6 py-4 text-right">{t('actions')}</th>
  122. </tr>
  123. </thead>
  124. <tbody className="divide-y divide-slate-100/80 text-sm">
  125. {importTasks.map(task => (
  126. <tr key={task.id} className="hover:bg-slate-50/50 transition-colors">
  127. <td className="px-6 py-4 text-slate-900 font-medium">
  128. {task.sourcePath}
  129. </td>
  130. <td className="px-6 py-4 text-slate-500">
  131. {groups.find((g: any) => g.id === task.targetGroupId)?.name || task.targetGroupName || task.sourcePath.split(/[\\/]/).pop() || task.targetGroupId || '-'}
  132. </td>
  133. <td className="px-6 py-4">
  134. {(() => {
  135. let colorClass = 'bg-amber-100 text-amber-700';
  136. if (task.status === 'COMPLETED') colorClass = 'bg-emerald-100 text-emerald-700';
  137. else if (task.status === 'FAILED') colorClass = 'bg-red-100 text-red-700';
  138. else if (task.status === 'PROCESSING') colorClass = 'bg-blue-100 text-blue-700';
  139. return (
  140. <span className={`px-2 py-1 rounded-md text-xs font-bold ${colorClass}`}>
  141. {task.status}
  142. </span>
  143. );
  144. })()}
  145. {task.status === 'FAILED' && task.logs && (
  146. <div className="text-xs text-red-500 mt-1 max-w-xs truncate" title={task.logs}>
  147. {task.logs}
  148. </div>
  149. )}
  150. </td>
  151. <td className="px-6 py-4 text-slate-500">
  152. {task.scheduledAt ? new Date(task.scheduledAt).toLocaleString() : '-'}
  153. </td>
  154. <td className="px-6 py-4 text-slate-400 text-xs">
  155. {new Date(task.createdAt).toLocaleString()}
  156. </td>
  157. <td className="px-6 py-4 text-right">
  158. <button
  159. onClick={() => handleDelete(task.id)}
  160. className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
  161. title={t('delete')}
  162. >
  163. <Trash2 size={16} />
  164. </button>
  165. </td>
  166. </tr>
  167. ))}
  168. </tbody>
  169. </table>
  170. </div>
  171. </div>
  172. )}
  173. </div>
  174. </div>
  175. <ConfirmDialog
  176. isOpen={!!taskToDelete}
  177. message={t('confirmDeleteTask')}
  178. onConfirm={confirmDelete}
  179. onCancel={() => setTaskToDelete(null)}
  180. />
  181. </div>
  182. );
  183. };