|
|
@@ -179,189 +179,190 @@ export default function IndexManagementPage() {
|
|
|
return (
|
|
|
<div className="max-w-7xl mx-auto px-6 py-8 lg:px-8">
|
|
|
<div className="space-y-6">
|
|
|
- <div>
|
|
|
- <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
|
|
- Index Management
|
|
|
- </h1>
|
|
|
- <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
|
- Index destination files for fast duplicate detection
|
|
|
- </p>
|
|
|
- </div>
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
|
|
+ Index Management
|
|
|
+ </h1>
|
|
|
+ <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
|
+ Index destination files for fast duplicate detection
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* Index Controls */}
|
|
|
- <div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
|
|
- <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
|
- Index Destination
|
|
|
- </h2>
|
|
|
-
|
|
|
- <div className="space-y-4">
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
- Dataset
|
|
|
- </label>
|
|
|
- <select
|
|
|
- value={selectedDataset}
|
|
|
- onChange={(e) => setSelectedDataset(e.target.value)}
|
|
|
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
- >
|
|
|
- <option value="">Select a dataset...</option>
|
|
|
- {datasetNames.map((name) => (
|
|
|
- <option key={name} value={name}>
|
|
|
- {name}
|
|
|
- </option>
|
|
|
- ))}
|
|
|
- </select>
|
|
|
- </div>
|
|
|
+ {/* Index Controls */}
|
|
|
+ <div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
|
|
+ <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
|
+ Index Destination
|
|
|
+ </h2>
|
|
|
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
- Destination Path
|
|
|
- </label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- value={destinationPath}
|
|
|
- onChange={(e) => setDestinationPath(e.target.value)}
|
|
|
- placeholder="/path/to/destination"
|
|
|
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Dataset
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ value={selectedDataset}
|
|
|
+ onChange={(e) => setSelectedDataset(e.target.value)}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
+ >
|
|
|
+ <option value="">Select a dataset...</option>
|
|
|
+ {datasetNames.map((name) => (
|
|
|
+ <option key={name} value={name}>
|
|
|
+ {name}
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
- Batch Size
|
|
|
- </label>
|
|
|
- <input
|
|
|
- type="number"
|
|
|
- value={batchSize}
|
|
|
- onChange={(e) => setBatchSize(parseInt(e.target.value))}
|
|
|
- min="10"
|
|
|
- max="1000"
|
|
|
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
- />
|
|
|
- <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
- Number of files to process at once
|
|
|
- </p>
|
|
|
- </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Destination Path
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={destinationPath}
|
|
|
+ onChange={(e) => setDestinationPath(e.target.value)}
|
|
|
+ placeholder="/path/to/destination"
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
- <div className="flex gap-2">
|
|
|
- <button
|
|
|
- onClick={() => handleIndex(false)}
|
|
|
- disabled={indexMutation.isPending}
|
|
|
- className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
|
- >
|
|
|
- <FolderIcon className="h-5 w-5 mr-2" />
|
|
|
- Index
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- onClick={() => handleIndex(true)}
|
|
|
- disabled={indexMutation.isPending}
|
|
|
- className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
|
|
|
- >
|
|
|
- <ArrowPathIcon className="h-5 w-5 mr-2" />
|
|
|
- Re-index
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- onClick={handleClear}
|
|
|
- disabled={clearMutation.isPending || !selectedDataset}
|
|
|
- className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
|
- >
|
|
|
- <TrashIcon className="h-5 w-5 mr-2" />
|
|
|
- Clear Index
|
|
|
- </button>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Batch Size
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={batchSize}
|
|
|
+ onChange={(e) => setBatchSize(parseInt(e.target.value))}
|
|
|
+ min="10"
|
|
|
+ max="1000"
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100"
|
|
|
+ />
|
|
|
+ <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ Number of files to process at once
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <button
|
|
|
+ onClick={() => handleIndex(false)}
|
|
|
+ disabled={indexMutation.isPending}
|
|
|
+ className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ <FolderIcon className="h-5 w-5 mr-2" />
|
|
|
+ Index
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button
|
|
|
+ onClick={() => handleIndex(true)}
|
|
|
+ disabled={indexMutation.isPending}
|
|
|
+ className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ <ArrowPathIcon className="h-5 w-5 mr-2" />
|
|
|
+ Re-index
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button
|
|
|
+ onClick={handleClear}
|
|
|
+ disabled={clearMutation.isPending || !selectedDataset}
|
|
|
+ className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ <TrashIcon className="h-5 w-5 mr-2" />
|
|
|
+ Clear Index
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- {/* Index Stats */}
|
|
|
- {selectedDataset && (
|
|
|
+ {/* Index Stats */}
|
|
|
+ {selectedDataset && (
|
|
|
+ <div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
|
|
+ <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
|
|
+ <ChartBarIcon className="h-5 w-5 mr-2" />
|
|
|
+ Index Statistics
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ {isLoadingCount ? (
|
|
|
+ <LoadingCard message="Loading stats..." />
|
|
|
+ ) : (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
|
+ <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
+ Indexed Files
|
|
|
+ </span>
|
|
|
+ <span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
|
+ {indexCount?.count || 0}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Duplicate Stats */}
|
|
|
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
|
|
- <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
|
|
- <ChartBarIcon className="h-5 w-5 mr-2" />
|
|
|
- Index Statistics
|
|
|
+ <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
|
+ Duplicate Statistics
|
|
|
</h2>
|
|
|
|
|
|
- {isLoadingCount ? (
|
|
|
- <LoadingCard message="Loading stats..." />
|
|
|
- ) : (
|
|
|
+ {isLoadingStats ? (
|
|
|
+ <LoadingCard message="Loading duplicate stats..." />
|
|
|
+ ) : stats && stats.totalDuplicates > 0 ? (
|
|
|
<div className="space-y-4">
|
|
|
- <div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
|
- <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
- Indexed Files
|
|
|
+ <div className="flex justify-between items-center p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
|
+ <span className="text-sm font-medium text-yellow-800 dark:text-yellow-300">
|
|
|
+ Total Duplicate Groups
|
|
|
</span>
|
|
|
- <span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
|
- {indexCount?.count || 0}
|
|
|
+ <span className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
|
+ {stats.totalDuplicates}
|
|
|
</span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- )}
|
|
|
|
|
|
- {/* Duplicate Stats */}
|
|
|
- <div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
|
|
- <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
|
- Duplicate Statistics
|
|
|
- </h2>
|
|
|
+ <div className="space-y-3 max-h-96 overflow-y-auto">
|
|
|
+ {stats.duplicatesByDataset.slice(0, 10).map((dup, idx) => (
|
|
|
+ <div
|
|
|
+ key={idx}
|
|
|
+ className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600"
|
|
|
+ >
|
|
|
+ <div className="flex justify-between items-start mb-2">
|
|
|
+ <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
+ [{dup.dataset}] {dup.file_count} files
|
|
|
+ </span>
|
|
|
+ <span className="text-sm text-gray-500 dark:text-gray-400">
|
|
|
+ {formatBytes(dup.file_size)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className="text-xs text-gray-500 dark:text-gray-400 font-mono mb-2">
|
|
|
+ Hash: {dup.hash.substring(0, 32)}...
|
|
|
+ </div>
|
|
|
+ <div className="space-y-1">
|
|
|
+ {dup.files.map((file, fileIdx) => (
|
|
|
+ <div
|
|
|
+ key={fileIdx}
|
|
|
+ className="text-xs text-gray-600 dark:text-gray-400 truncate"
|
|
|
+ title={file}
|
|
|
+ >
|
|
|
+ • {file}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
|
|
|
- {isLoadingStats ? (
|
|
|
- <LoadingCard message="Loading duplicate stats..." />
|
|
|
- ) : stats && stats.totalDuplicates > 0 ? (
|
|
|
- <div className="space-y-4">
|
|
|
- <div className="flex justify-between items-center p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
|
- <span className="text-sm font-medium text-yellow-800 dark:text-yellow-300">
|
|
|
- Total Duplicate Groups
|
|
|
- </span>
|
|
|
- <span className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
|
- {stats.totalDuplicates}
|
|
|
- </span>
|
|
|
+ {stats.duplicatesByDataset.length > 10 && (
|
|
|
+ <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
|
+ ... and {stats.duplicatesByDataset.length - 10} more duplicate
|
|
|
+ groups
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
-
|
|
|
- <div className="space-y-3 max-h-96 overflow-y-auto">
|
|
|
- {stats.duplicatesByDataset.slice(0, 10).map((dup, idx) => (
|
|
|
- <div
|
|
|
- key={idx}
|
|
|
- className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600"
|
|
|
- >
|
|
|
- <div className="flex justify-between items-start mb-2">
|
|
|
- <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
- [{dup.dataset}] {dup.file_count} files
|
|
|
- </span>
|
|
|
- <span className="text-sm text-gray-500 dark:text-gray-400">
|
|
|
- {formatBytes(dup.file_size)}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="text-xs text-gray-500 dark:text-gray-400 font-mono mb-2">
|
|
|
- Hash: {dup.hash.substring(0, 32)}...
|
|
|
- </div>
|
|
|
- <div className="space-y-1">
|
|
|
- {dup.files.map((file, fileIdx) => (
|
|
|
- <div
|
|
|
- key={fileIdx}
|
|
|
- className="text-xs text-gray-600 dark:text-gray-400 truncate"
|
|
|
- title={file}
|
|
|
- >
|
|
|
- • {file}
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
+ ) : (
|
|
|
+ <div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
|
+ <p>No duplicates found in indexed files</p>
|
|
|
</div>
|
|
|
-
|
|
|
- {stats.duplicatesByDataset.length > 10 && (
|
|
|
- <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
|
- ... and {stats.duplicatesByDataset.length - 10} more duplicate
|
|
|
- groups
|
|
|
- </p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
|
- <p>No duplicates found in indexed files</p>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|