ImportTasksDrawer.tsx 9.4 KB

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