| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- import React, { useState, useEffect } from 'react';
- import { X, Box, Loader2, Trash2 } from 'lucide-react';
- import { importService, ImportTask } from '../../services/importService';
- import { useLanguage } from '../../contexts/LanguageContext';
- import { knowledgeGroupService } from '../../services/knowledgeGroupService';
- import { KnowledgeGroup } from '../../types';
- import { useToast } from '../../contexts/ToastContext';
- import ConfirmDialog from '../ConfirmDialog';
- interface ImportTasksDrawerProps {
- isOpen: boolean;
- onClose: () => void;
- authToken: string;
- }
- export const ImportTasksDrawer: React.FC<ImportTasksDrawerProps> = ({
- isOpen,
- onClose,
- authToken,
- }) => {
- const { t } = useLanguage();
- const { showError } = useToast();
- const [importTasks, setImportTasks] = useState<ImportTask[]>([]);
- const [groups, setGroups] = useState<KnowledgeGroup[]>([]);
- const [isLoading, setIsLoading] = useState(false);
- const [taskToDelete, setTaskToDelete] = useState<string | null>(null);
- const fetchData = async () => {
- if (!authToken) return;
- try {
- setIsLoading(true);
- const [tasksResult, groupsData] = await Promise.all([
- importService.getAll(authToken),
- knowledgeGroupService.getGroups()
- ]);
- setImportTasks(tasksResult.items);
- // Flatten the groups tree so we can easily find names by ID
- const flat: KnowledgeGroup[] = [];
- const walk = (items: KnowledgeGroup[]) => {
- for (const g of items) {
- flat.push(g);
- if (g.children?.length) walk(g.children);
- }
- };
- if (groupsData) walk(groupsData);
- setGroups(flat);
- } catch (error) {
- console.error('Failed to fetch data:', error);
- } finally {
- setIsLoading(false);
- }
- };
- const handleDelete = async (taskId: string) => {
- setTaskToDelete(taskId);
- };
- const confirmDelete = async () => {
- if (!taskToDelete) return;
- try {
- await importService.delete(authToken, taskToDelete);
- fetchData();
- } catch (error) {
- console.error('Failed to delete task:', error);
- showError(t('deleteTaskFailed'));
- } finally {
- setTaskToDelete(null);
- }
- };
- useEffect(() => {
- if (isOpen) {
- fetchData();
- }
- }, [isOpen, authToken]);
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 z-50 flex justify-end">
- <div className="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity" onClick={onClose} />
- <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">
- {/* Header */}
- <div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between shrink-0 bg-white">
- <div className="flex items-center gap-3">
- <div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600">
- <Box size={16} />
- </div>
- <h2 className="text-lg font-bold text-slate-800">{t('importTasksTitle')}</h2>
- </div>
- <div className="flex items-center gap-2">
- <button
- onClick={fetchData}
- className="p-2 text-slate-400 hover:text-indigo-600 hover:bg-slate-50 rounded-lg transition-all"
- title={t('refresh')}
- >
- <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>
- </button>
- <button
- onClick={onClose}
- className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-colors"
- >
- <X size={20} />
- </button>
- </div>
- </div>
- {/* Body */}
- <div className="flex-1 overflow-y-auto p-6 bg-slate-50/30">
- {isLoading ? (
- <div className="p-12 flex justify-center">
- <Loader2 className="animate-spin text-slate-300 w-8 h-8" />
- </div>
- ) : importTasks.length === 0 ? (
- <div className="p-12 text-center text-slate-400 flex flex-col items-center">
- <Box size={48} className="mb-4 opacity-20" />
- <span className="text-sm font-bold uppercase tracking-widest">{t('noTasksFound')}</span>
- </div>
- ) : (
- <div className="bg-white rounded-2xl border border-slate-200/60 shadow-sm overflow-hidden">
- <div className="overflow-x-auto">
- <table className="w-full text-left">
- <thead>
- <tr className="border-b border-slate-200/60 bg-slate-50/50 text-[10px] font-black uppercase tracking-widest text-slate-500">
- <th className="px-6 py-4">{t('sourcePath')}</th>
- <th className="px-6 py-4">{t('targetGroup')}</th>
- <th className="px-6 py-4">{t('status')}</th>
- <th className="px-6 py-4">{t('scheduledAt')}</th>
- <th className="px-6 py-4">{t('createdAt')}</th>
- <th className="px-6 py-4 text-right">{t('actions')}</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-slate-100/80 text-sm">
- {importTasks.map(task => (
- <tr key={task.id} className="hover:bg-slate-50/50 transition-colors">
- <td className="px-6 py-4 text-slate-900 font-medium">
- {task.sourcePath}
- </td>
- <td className="px-6 py-4 text-slate-500">
- {groups.find((g: any) => g.id === task.targetGroupId)?.name || task.targetGroupName || task.sourcePath.split(/[\\/]/).pop() || task.targetGroupId || '-'}
- </td>
- <td className="px-6 py-4">
- {(() => {
- let colorClass = 'bg-amber-100 text-amber-700';
- if (task.status === 'COMPLETED') colorClass = 'bg-emerald-100 text-emerald-700';
- else if (task.status === 'FAILED') colorClass = 'bg-red-100 text-red-700';
- else if (task.status === 'PROCESSING') colorClass = 'bg-blue-100 text-blue-700';
- return (
- <span className={`px-2 py-1 rounded-md text-xs font-bold ${colorClass}`}>
- {task.status}
- </span>
- );
- })()}
- {task.status === 'FAILED' && task.logs && (
- <div className="text-xs text-red-500 mt-1 max-w-xs truncate" title={task.logs}>
- {task.logs}
- </div>
- )}
- </td>
- <td className="px-6 py-4 text-slate-500">
- {task.scheduledAt ? new Date(task.scheduledAt).toLocaleString() : '-'}
- </td>
- <td className="px-6 py-4 text-slate-400 text-xs">
- {new Date(task.createdAt).toLocaleString()}
- </td>
- <td className="px-6 py-4 text-right">
- <button
- onClick={() => handleDelete(task.id)}
- className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
- title={t('delete')}
- >
- <Trash2 size={16} />
- </button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- </div>
- <ConfirmDialog
- isOpen={!!taskToDelete}
- message={t('confirmDeleteTask')}
- onConfirm={confirmDelete}
- onCancel={() => setTaskToDelete(null)}
- />
- </div>
- );
- };
|