ImportTasksDrawer.tsx 9.8 KB

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