Jelajahi Sumber

feat: add JSON editing mode to DatasetCrud component

- Add isJsonMode state toggle to switch between form and JSON editing modes
- Add handleJsonChange handler to parse and update config from JSON input
- Display JSON textarea editor showing current config with pretty-printed formatting
- Allow users to edit dataset configuration at the subcomponent level using either form UI or raw JSON
- Complements the existing JSON mode in DatasetsSettingsEditor for top-level editing
Timothy Pomeroy 1 bulan lalu
induk
melakukan
a365a0fbba
1 mengubah file dengan 155 tambahan dan 103 penghapusan
  1. 155 103
      apps/web/src/app/components/DatasetCrud.tsx

+ 155 - 103
apps/web/src/app/components/DatasetCrud.tsx

@@ -28,6 +28,7 @@ export default function DatasetCrud({
   const [config, setConfig] = useState<DatasetConfig>(datasetConfig);
   const [newPath, setNewPath] = useState("");
   const [enabled, setEnabled] = useState(true);
+  const [isJsonMode, setIsJsonMode] = useState(false);
 
   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 (
     <SlideInForm
       isOpen={isOpen}
@@ -109,115 +119,157 @@ export default function DatasetCrud({
       }
     >
       <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>
 
-        {/* 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>
-          ) : (
-            <p className="text-sm text-gray-500 italic text-center py-4">
-              No paths configured. Add a path above.
-            </p>
-          )}
-        </div>
+          </>
+        )}
       </form>
     </SlideInForm>
   );