|
@@ -28,6 +28,7 @@ export default function DatasetCrud({
|
|
|
const [config, setConfig] = useState<DatasetConfig>(datasetConfig);
|
|
const [config, setConfig] = useState<DatasetConfig>(datasetConfig);
|
|
|
const [newPath, setNewPath] = useState("");
|
|
const [newPath, setNewPath] = useState("");
|
|
|
const [enabled, setEnabled] = useState(true);
|
|
const [enabled, setEnabled] = useState(true);
|
|
|
|
|
+ const [isJsonMode, setIsJsonMode] = useState(false);
|
|
|
|
|
|
|
|
const isEditing = !!datasetName;
|
|
const isEditing = !!datasetName;
|
|
|
|
|
|
|
@@ -84,6 +85,15 @@ export default function DatasetCrud({
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleJsonChange = (jsonValue: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsedConfig = JSON.parse(jsonValue);
|
|
|
|
|
+ setConfig(parsedConfig);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // Invalid JSON, ignore for now
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<SlideInForm
|
|
<SlideInForm
|
|
|
isOpen={isOpen}
|
|
isOpen={isOpen}
|
|
@@ -109,115 +119,157 @@ export default function DatasetCrud({
|
|
|
}
|
|
}
|
|
|
>
|
|
>
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
|
- {/* Dataset Name */}
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
|
|
|
|
- Dataset Name
|
|
|
|
|
- </label>
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- value={name}
|
|
|
|
|
- onChange={(e) => setName(e.target.value)}
|
|
|
|
|
- placeholder="e.g., pr0n, kids, movies"
|
|
|
|
|
- className="block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
|
|
|
|
- required
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ {/* JSON Mode Toggle */}
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <span className="text-xs text-gray-500">JSON Mode</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setIsJsonMode(!isJsonMode)}
|
|
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
|
|
|
|
+ isJsonMode ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <span
|
|
|
|
|
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
|
|
|
+ isJsonMode ? "translate-x-6" : "translate-x-1"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ />
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* Enabled Toggle */}
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
|
|
|
|
- Status
|
|
|
|
|
- </label>
|
|
|
|
|
- <div className="flex items-center">
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => handleEnabledChange(!enabled)}
|
|
|
|
|
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
|
|
|
|
- enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- <span
|
|
|
|
|
- className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
|
|
|
- enabled ? "translate-x-6" : "translate-x-1"
|
|
|
|
|
- }`}
|
|
|
|
|
|
|
+ {isJsonMode ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* JSON Editor Mode */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
|
|
|
|
+ Configuration JSON
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ value={JSON.stringify(config, null, 2)}
|
|
|
|
|
+ onChange={(e) => handleJsonChange(e.target.value)}
|
|
|
|
|
+ rows={12}
|
|
|
|
|
+ className="block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-mono"
|
|
|
|
|
+ placeholder='{"path": {}, ...}'
|
|
|
/>
|
|
/>
|
|
|
- </button>
|
|
|
|
|
- <span className="ml-3 text-sm text-gray-700 dark:text-gray-200">
|
|
|
|
|
- {enabled ? "Enabled" : "Disabled"}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-xs text-gray-500 mt-1">
|
|
|
|
|
- When disabled, this dataset will not be monitored for new files.
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-2">
|
|
|
|
|
+ Edit the dataset configuration as JSON. Each key is a path, with
|
|
|
|
|
+ nested settings.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* Form Mode */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
|
|
|
|
+ Dataset Name
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ value={name}
|
|
|
|
|
+ onChange={(e) => setName(e.target.value)}
|
|
|
|
|
+ placeholder="e.g., pr0n, kids, movies"
|
|
|
|
|
+ className="block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
|
|
|
|
+ required
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* Paths Configuration */}
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
|
|
|
|
- Watch Paths
|
|
|
|
|
- </label>
|
|
|
|
|
- <p className="text-xs text-gray-500 mb-4">
|
|
|
|
|
- Add paths to watch for this dataset. Each path can have its own
|
|
|
|
|
- configuration.
|
|
|
|
|
- </p>
|
|
|
|
|
-
|
|
|
|
|
- {/* Add new path */}
|
|
|
|
|
- <div className="flex gap-2 mb-4">
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- value={newPath}
|
|
|
|
|
- onChange={(e) => setNewPath(e.target.value)}
|
|
|
|
|
- placeholder="/path/to/watch"
|
|
|
|
|
- className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={addPath}
|
|
|
|
|
- className="inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
|
|
|
|
- >
|
|
|
|
|
- Add Path
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Existing paths */}
|
|
|
|
|
- {Object.entries(config).filter(([key]) => key !== "enabled").length >
|
|
|
|
|
- 0 ? (
|
|
|
|
|
- <div className="space-y-3">
|
|
|
|
|
- {Object.entries(config)
|
|
|
|
|
- .filter(([key]) => key !== "enabled")
|
|
|
|
|
- .map(([path, pathConfig]) => (
|
|
|
|
|
- <div
|
|
|
|
|
- key={path}
|
|
|
|
|
- className="border rounded-lg p-3 bg-gray-50 dark:bg-gray-800"
|
|
|
|
|
- >
|
|
|
|
|
- <div className="flex items-center justify-between mb-2">
|
|
|
|
|
- <span className="text-sm font-mono text-gray-900 dark:text-gray-100">
|
|
|
|
|
- {path}
|
|
|
|
|
- </span>
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => removePath(path)}
|
|
|
|
|
- className="inline-flex items-center rounded-md bg-red-600 px-2 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
|
|
|
|
|
+ {/* Enabled Toggle */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
|
|
|
|
+ Status
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="flex items-center">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => handleEnabledChange(!enabled)}
|
|
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
|
|
|
|
+ enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <span
|
|
|
|
|
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
|
|
|
+ enabled ? "translate-x-6" : "translate-x-1"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <span className="ml-3 text-sm text-gray-700 dark:text-gray-200">
|
|
|
|
|
+ {enabled ? "Enabled" : "Disabled"}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-1">
|
|
|
|
|
+ When disabled, this dataset will not be monitored for new files.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Paths Configuration */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
|
|
|
|
+ Watch Paths
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mb-4">
|
|
|
|
|
+ Add paths to watch for this dataset. Each path can have its own
|
|
|
|
|
+ configuration.
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Add new path */}
|
|
|
|
|
+ <div className="flex gap-2 mb-4">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ value={newPath}
|
|
|
|
|
+ onChange={(e) => setNewPath(e.target.value)}
|
|
|
|
|
+ placeholder="/path/to/watch"
|
|
|
|
|
+ className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
|
|
|
|
+ />
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={addPath}
|
|
|
|
|
+ className="inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ Add Path
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Existing paths */}
|
|
|
|
|
+ {Object.entries(config).filter(([key]) => key !== "enabled")
|
|
|
|
|
+ .length > 0 ? (
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {Object.entries(config)
|
|
|
|
|
+ .filter(([key]) => key !== "enabled")
|
|
|
|
|
+ .map(([path, pathConfig]) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={path}
|
|
|
|
|
+ className="border rounded-lg p-3 bg-gray-50 dark:bg-gray-800"
|
|
|
>
|
|
>
|
|
|
- Remove
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <PathConfigEditor
|
|
|
|
|
- value={pathConfig}
|
|
|
|
|
- onChange={(newConfig) =>
|
|
|
|
|
- updatePathConfig(path, newConfig)
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <span className="text-sm font-mono text-gray-900 dark:text-gray-100">
|
|
|
|
|
+ {path}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => removePath(path)}
|
|
|
|
|
+ className="inline-flex items-center rounded-md bg-red-600 px-2 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ Remove
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <PathConfigEditor
|
|
|
|
|
+ value={pathConfig}
|
|
|
|
|
+ onChange={(newConfig) =>
|
|
|
|
|
+ updatePathConfig(path, newConfig)
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-sm text-gray-500 italic text-center py-4">
|
|
|
|
|
+ No paths configured. Add a path above.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- ) : (
|
|
|
|
|
- <p className="text-sm text-gray-500 italic text-center py-4">
|
|
|
|
|
- No paths configured. Add a path above.
|
|
|
|
|
- </p>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
</form>
|
|
</form>
|
|
|
</SlideInForm>
|
|
</SlideInForm>
|
|
|
);
|
|
);
|