DatasetCrud.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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 isEditing = !!datasetName;
  29. useEffect(() => {
  30. setName(datasetName);
  31. setConfig(datasetConfig);
  32. // Set enabled state from existing config, defaulting to true
  33. setEnabled(
  34. datasetConfig.enabled !== undefined ? datasetConfig.enabled : true
  35. );
  36. }, [datasetName, datasetConfig]);
  37. const handleEnabledChange = (newEnabled: boolean) => {
  38. setEnabled(newEnabled);
  39. if (onEnabledChange) {
  40. onEnabledChange(newEnabled);
  41. }
  42. };
  43. const handleSubmit = (e: React.FormEvent) => {
  44. e.preventDefault();
  45. if (!name.trim()) return;
  46. // Note: enabled is handled separately via onEnabledChange
  47. // so we don't include it in the config here
  48. onSave(name, config);
  49. };
  50. const addPath = () => {
  51. if (!newPath.trim()) return;
  52. setConfig({
  53. ...config,
  54. [newPath]: {}
  55. });
  56. setNewPath("");
  57. };
  58. const removePath = (path: string) => {
  59. const newConfig = { ...config };
  60. delete newConfig[path];
  61. setConfig(newConfig);
  62. };
  63. const updatePathConfig = (path: string, pathConfig: any) => {
  64. try {
  65. const parsedConfig =
  66. typeof pathConfig === "string" ? JSON.parse(pathConfig) : pathConfig;
  67. setConfig({
  68. ...config,
  69. [path]: parsedConfig
  70. });
  71. } catch {
  72. // Invalid JSON, ignore
  73. }
  74. };
  75. return (
  76. <SlideInForm
  77. isOpen={isOpen}
  78. onClose={onClose}
  79. title={isEditing ? `Edit Dataset: ${datasetName}` : "Add New Dataset"}
  80. actions={
  81. <div className="flex justify-end space-x-3">
  82. <button
  83. type="button"
  84. onClick={onClose}
  85. 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"
  86. >
  87. Cancel
  88. </button>
  89. <button
  90. type="submit"
  91. onClick={handleSubmit}
  92. 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"
  93. >
  94. {isEditing ? "Update Dataset" : "Add Dataset"}
  95. </button>
  96. </div>
  97. }
  98. >
  99. <form onSubmit={handleSubmit} className="space-y-6">
  100. {/* Dataset Name */}
  101. <div>
  102. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
  103. Dataset Name
  104. </label>
  105. <input
  106. type="text"
  107. value={name}
  108. onChange={(e) => setName(e.target.value)}
  109. placeholder="e.g., pr0n, kids, movies"
  110. 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"
  111. required
  112. />
  113. </div>
  114. {/* Enabled Toggle */}
  115. <div>
  116. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
  117. Status
  118. </label>
  119. <div className="flex items-center">
  120. <button
  121. type="button"
  122. onClick={() => handleEnabledChange(!enabled)}
  123. 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 ${
  124. enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-700"
  125. }`}
  126. >
  127. <span
  128. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  129. enabled ? "translate-x-6" : "translate-x-1"
  130. }`}
  131. />
  132. </button>
  133. <span className="ml-3 text-sm text-gray-700 dark:text-gray-200">
  134. {enabled ? "Enabled" : "Disabled"}
  135. </span>
  136. </div>
  137. <p className="text-xs text-gray-500 mt-1">
  138. When disabled, this dataset will not be monitored for new files.
  139. </p>
  140. </div>
  141. {/* Paths Configuration */}
  142. <div>
  143. <label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
  144. Watch Paths
  145. </label>
  146. <p className="text-xs text-gray-500 mb-4">
  147. Add paths to watch for this dataset. Each path can have its own
  148. configuration.
  149. </p>
  150. {/* Add new path */}
  151. <div className="flex gap-2 mb-4">
  152. <input
  153. type="text"
  154. value={newPath}
  155. onChange={(e) => setNewPath(e.target.value)}
  156. placeholder="/path/to/watch"
  157. 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"
  158. />
  159. <button
  160. type="button"
  161. onClick={addPath}
  162. 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"
  163. >
  164. Add Path
  165. </button>
  166. </div>
  167. {/* Existing paths */}
  168. {Object.entries(config).filter(([key]) => key !== "enabled").length >
  169. 0 ? (
  170. <div className="space-y-3">
  171. {Object.entries(config)
  172. .filter(([key]) => key !== "enabled")
  173. .map(([path, pathConfig]) => (
  174. <div
  175. key={path}
  176. className="border rounded-lg p-3 bg-gray-50 dark:bg-gray-800"
  177. >
  178. <div className="flex items-center justify-between mb-2">
  179. <span className="text-sm font-mono text-gray-900 dark:text-gray-100">
  180. {path}
  181. </span>
  182. <button
  183. type="button"
  184. onClick={() => removePath(path)}
  185. 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"
  186. >
  187. Remove
  188. </button>
  189. </div>
  190. <PathConfigEditor
  191. value={pathConfig}
  192. onChange={(newConfig) =>
  193. updatePathConfig(path, newConfig)
  194. }
  195. />
  196. </div>
  197. ))}
  198. </div>
  199. ) : (
  200. <p className="text-sm text-gray-500 italic text-center py-4">
  201. No paths configured. Add a path above.
  202. </p>
  203. )}
  204. </div>
  205. </form>
  206. </SlideInForm>
  207. );
  208. }