DatasetCrud.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import PathConfigEditor from "./PathConfigEditor";
  4. import SlideInForm from "./SlideInForm";
  5. interface DatasetConfig {
  6. [path: string]: any;
  7. }
  8. interface DatasetCrudProps {
  9. datasetName?: string;
  10. datasetConfig?: DatasetConfig;
  11. onSave: (name: string, config: DatasetConfig) => void;
  12. onClose: () => void;
  13. onEnabledChange?: (enabled: boolean) => void;
  14. isOpen: boolean;
  15. }
  16. export default function DatasetCrud({
  17. datasetName = "",
  18. datasetConfig = {},
  19. onSave,
  20. onClose,
  21. onEnabledChange,
  22. isOpen
  23. }: DatasetCrudProps) {
  24. const [name, setName] = useState(datasetName);
  25. const [config, setConfig] = useState<DatasetConfig>(datasetConfig);
  26. const [newPath, setNewPath] = useState("");
  27. const [enabled, setEnabled] = useState(true);
  28. const [isJsonMode, setIsJsonMode] = useState(false);
  29. const isEditing = !!datasetName;
  30. useEffect(() => {
  31. setName(datasetName);
  32. setConfig(datasetConfig);
  33. // Set enabled state from existing config, defaulting to true
  34. setEnabled(
  35. datasetConfig.enabled !== undefined ? datasetConfig.enabled : true
  36. );
  37. }, [datasetName, datasetConfig]);
  38. const handleEnabledChange = (newEnabled: boolean) => {
  39. setEnabled(newEnabled);
  40. if (onEnabledChange) {
  41. onEnabledChange(newEnabled);
  42. }
  43. };
  44. const handleSubmit = (e: React.FormEvent) => {
  45. e.preventDefault();
  46. if (!name.trim()) return;
  47. // Note: enabled is handled separately via onEnabledChange
  48. // so we don't include it in the config here
  49. onSave(name, config);
  50. };
  51. const addPath = () => {
  52. if (!newPath.trim()) return;
  53. setConfig({
  54. ...config,
  55. [newPath]: {}
  56. });
  57. setNewPath("");
  58. };
  59. const removePath = (path: string) => {
  60. const newConfig = { ...config };
  61. delete newConfig[path];
  62. setConfig(newConfig);
  63. };
  64. const updatePathConfig = (path: string, pathConfig: any) => {
  65. try {
  66. const parsedConfig =
  67. typeof pathConfig === "string" ? JSON.parse(pathConfig) : pathConfig;
  68. setConfig({
  69. ...config,
  70. [path]: parsedConfig
  71. });
  72. } catch {
  73. // Invalid JSON, ignore
  74. }
  75. };
  76. const handleJsonChange = (jsonValue: string) => {
  77. try {
  78. const parsedConfig = JSON.parse(jsonValue);
  79. setConfig(parsedConfig);
  80. } catch {
  81. // Invalid JSON, ignore for now
  82. }
  83. };
  84. return (
  85. <SlideInForm
  86. isOpen={isOpen}
  87. onClose={onClose}
  88. title={isEditing ? `Edit Dataset: ${datasetName}` : "Add New Dataset"}
  89. actions={
  90. <div className="flex justify-end space-x-3">
  91. <button
  92. type="button"
  93. onClick={onClose}
  94. className="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
  95. >
  96. Cancel
  97. </button>
  98. <button
  99. type="submit"
  100. onClick={handleSubmit}
  101. className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
  102. >
  103. {isEditing ? "Update Dataset" : "Add Dataset"}
  104. </button>
  105. </div>
  106. }
  107. >
  108. <form onSubmit={handleSubmit} className="space-y-6">
  109. {/* JSON Mode Toggle */}
  110. <div className="flex items-center justify-between">
  111. <span className="text-xs text-gray-500">JSON Mode</span>
  112. <button
  113. type="button"
  114. onClick={() => setIsJsonMode(!isJsonMode)}
  115. 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 ${
  116. isJsonMode ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
  117. }`}
  118. >
  119. <span
  120. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  121. isJsonMode ? "translate-x-6" : "translate-x-1"
  122. }`}
  123. />
  124. </button>
  125. </div>
  126. {isJsonMode ? (
  127. <>
  128. {/* JSON Editor Mode */}
  129. <div>
  130. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
  131. Configuration JSON
  132. </label>
  133. <textarea
  134. value={JSON.stringify(config, null, 2)}
  135. onChange={(e) => handleJsonChange(e.target.value)}
  136. rows={12}
  137. 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"
  138. placeholder='{"path": {}, ...}'
  139. />
  140. <p className="text-xs text-gray-500 mt-2">
  141. Edit the dataset configuration as JSON. Each key is a path, with
  142. nested settings.
  143. </p>
  144. </div>
  145. </>
  146. ) : (
  147. <>
  148. {/* Form Mode */}
  149. <div>
  150. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
  151. Dataset Name
  152. </label>
  153. <input
  154. type="text"
  155. value={name}
  156. onChange={(e) => setName(e.target.value)}
  157. placeholder="e.g., pr0n, kids, movies"
  158. 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"
  159. required
  160. />
  161. </div>
  162. {/* Enabled Toggle */}
  163. <div>
  164. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
  165. Status
  166. </label>
  167. <div className="flex items-center">
  168. <button
  169. type="button"
  170. onClick={() => handleEnabledChange(!enabled)}
  171. 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 ${
  172. enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
  173. }`}
  174. >
  175. <span
  176. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  177. enabled ? "translate-x-6" : "translate-x-1"
  178. }`}
  179. />
  180. </button>
  181. <span className="ml-3 text-sm text-gray-700 dark:text-gray-200">
  182. {enabled ? "Enabled" : "Disabled"}
  183. </span>
  184. </div>
  185. <p className="text-xs text-gray-500 mt-1">
  186. When disabled, this dataset will not be monitored for new files.
  187. </p>
  188. </div>
  189. {/* Paths Configuration */}
  190. <div>
  191. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
  192. Watch Paths
  193. </label>
  194. <p className="text-xs text-gray-500 mb-4">
  195. Add paths to watch for this dataset. Each path can have its own
  196. configuration.
  197. </p>
  198. {/* Add new path */}
  199. <div className="flex gap-2 mb-4">
  200. <input
  201. type="text"
  202. value={newPath}
  203. onChange={(e) => setNewPath(e.target.value)}
  204. placeholder="/path/to/watch"
  205. 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"
  206. />
  207. <button
  208. type="button"
  209. onClick={addPath}
  210. 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"
  211. >
  212. Add Path
  213. </button>
  214. </div>
  215. {/* Existing paths */}
  216. {Object.entries(config).filter(([key]) => key !== "enabled")
  217. .length > 0 ? (
  218. <div className="space-y-3">
  219. {Object.entries(config)
  220. .filter(([key]) => key !== "enabled")
  221. .map(([path, pathConfig]) => (
  222. <div
  223. key={path}
  224. className="border rounded-lg p-3 bg-gray-50 dark:bg-gray-800"
  225. >
  226. <div className="flex items-center justify-between mb-2">
  227. <span className="text-sm font-mono text-gray-900 dark:text-gray-100">
  228. {path}
  229. </span>
  230. <button
  231. type="button"
  232. onClick={() => removePath(path)}
  233. 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"
  234. >
  235. Remove
  236. </button>
  237. </div>
  238. <PathConfigEditor
  239. value={pathConfig}
  240. onChange={(newConfig) =>
  241. updatePathConfig(path, newConfig)
  242. }
  243. />
  244. </div>
  245. ))}
  246. </div>
  247. ) : (
  248. <p className="text-sm text-gray-500 italic text-center py-4">
  249. No paths configured. Add a path above.
  250. </p>
  251. )}
  252. </div>
  253. </>
  254. )}
  255. </form>
  256. </SlideInForm>
  257. );
  258. }