Просмотр исходного кода

fix(frontend): FinalMaskForm TCP Mask sub-forms + Advanced JSON wrap (B10/B11)

B10 — FinalMaskForm TCP Mask: after adding a mask and picking a Type
(Fragment/Header Custom/Sudoku), the type-specific sub-forms didn't
render. TcpMaskItem read `type` via Form.useWatch on a path inside
Form.List, which doesn't re-fire reliably in AntD 6.4.3 — same root
cause as the earlier B1/B2/B5 reactivity issues. Replaced with a
<Form.Item shouldUpdate> wrapper that reads `type` via getFieldValue
inside the render prop.

B11 — Advanced sub-tabs (settings / streamSettings / sniffing) showed
just the inner value (e.g. `{clients:[],decryption:"none",...}`), but
the legacy modal wrapped each slice with its key envelope (e.g.
`{settings:{...}}`) so the JSON matches the wire shape's slice and
round-trips cleanly from copy-pasted inbound configs. Added a
`wrapKey` prop to AdvancedSliceEditor that wraps/unwraps the value
on render/write; the three sub-tabs now pass settings / streamSettings
/ sniffing as their wrapKey.
MHSanaei 1 день назад
Родитель
Сommit
36afdf53af

+ 54 - 42
frontend/src/components/FinalMaskForm.tsx

@@ -156,7 +156,6 @@ function TcpMaskItem({
   onRemove: () => void;
 }) {
   const path = [...base, 'tcp', index];
-  const type = Form.useWatch([...path, 'type'], form) as string | undefined;
 
   return (
     <div>
@@ -176,47 +175,60 @@ function TcpMaskItem({
         />
       </Form.Item>
 
-      {type === 'fragment' && (
-        <>
-          <Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
-            <Select
-              options={[
-                { value: 'tlshello', label: 'tlshello' },
-                { value: '1-3', label: '1-3' },
-                { value: '1-5', label: '1-5' },
-              ]}
-            />
-          </Form.Item>
-          <Form.Item label="Length" name={[...path, 'settings', 'length']}>
-            <Input />
-          </Form.Item>
-          <Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
-            <Input />
-          </Form.Item>
-          <Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
-            <Input />
-          </Form.Item>
-        </>
-      )}
-
-      {type === 'sudoku' && (
-        <>
-          <Form.Item label="Password" name={[...path, 'settings', 'password']}><Input /></Form.Item>
-          <Form.Item label="ASCII" name={[...path, 'settings', 'ascii']}><Input /></Form.Item>
-          <Form.Item label="Custom Table" name={[...path, 'settings', 'customTable']}><Input /></Form.Item>
-          <Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></Form.Item>
-          <Form.Item label="Padding Min" name={[...path, 'settings', 'paddingMin']}>
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.Item label="Padding Max" name={[...path, 'settings', 'paddingMax']}>
-            <InputNumber min={0} />
-          </Form.Item>
-        </>
-      )}
-
-      {type === 'header-custom' && (
-        <HeaderCustomGroups base={[...path, 'settings']} form={form} />
-      )}
+      <Form.Item
+        noStyle
+        shouldUpdate={(prev, curr) =>
+          (prev as Record<string, unknown>)[String(path[0])] !== (curr as Record<string, unknown>)[String(path[0])]
+        }
+      >
+        {({ getFieldValue }) => {
+          const type = getFieldValue([...path, 'type']) as string | undefined;
+          if (type === 'fragment') {
+            return (
+              <>
+                <Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
+                  <Select
+                    options={[
+                      { value: 'tlshello', label: 'tlshello' },
+                      { value: '1-3', label: '1-3' },
+                      { value: '1-5', label: '1-5' },
+                    ]}
+                  />
+                </Form.Item>
+                <Form.Item label="Length" name={[...path, 'settings', 'length']}>
+                  <Input />
+                </Form.Item>
+                <Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
+                  <Input />
+                </Form.Item>
+                <Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
+                  <Input />
+                </Form.Item>
+              </>
+            );
+          }
+          if (type === 'sudoku') {
+            return (
+              <>
+                <Form.Item label="Password" name={[...path, 'settings', 'password']}><Input /></Form.Item>
+                <Form.Item label="ASCII" name={[...path, 'settings', 'ascii']}><Input /></Form.Item>
+                <Form.Item label="Custom Table" name={[...path, 'settings', 'customTable']}><Input /></Form.Item>
+                <Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></Form.Item>
+                <Form.Item label="Padding Min" name={[...path, 'settings', 'paddingMin']}>
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item label="Padding Max" name={[...path, 'settings', 'paddingMax']}>
+                  <InputNumber min={0} />
+                </Form.Item>
+              </>
+            );
+          }
+          if (type === 'header-custom') {
+            return <HeaderCustomGroups base={[...path, 'settings']} form={form} />;
+          }
+          return null;
+        }}
+      </Form.Item>
     </div>
   );
 }

+ 23 - 10
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -93,33 +93,40 @@ const { Text } = Typography;
 function AdvancedSliceEditor({
   form,
   path,
+  wrapKey,
   minHeight,
   maxHeight,
 }: {
   form: FormInstance<InboundFormValues>;
   path: NamePath;
+  // When set, the editor wraps the inner value with `{ [wrapKey]: ... }` so
+  // the JSON the user sees matches the wire shape's slice envelope (e.g.
+  // `{ "settings": { ... } }`). Edits unwrap the outer key before writing
+  // back to the form. Mirrors the legacy modal's wrappedConfigValue.
+  wrapKey?: string;
   minHeight?: string;
   maxHeight?: string;
 }) {
-  // The editor keeps a local text buffer so partial / invalid JSON typing
-  // doesn't clobber the form. lastEmitRef tracks the serialized form value
-  // at the moment we last accepted a write — if useWatch later fires with
-  // a different value than that, the form was changed from elsewhere
-  // (Stream tab toggle, sibling JSON tab edit), and we re-sync.
+  const serialize = (value: unknown): string => {
+    const inner = value ?? {};
+    return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
+  };
+
   const watched = Form.useWatch(path, form);
   const lastEmitRef = useRef<string>('');
   const [text, setText] = useState(() => {
-    const initial = JSON.stringify(form.getFieldValue(path) ?? {}, null, 2);
+    const initial = serialize(form.getFieldValue(path));
     lastEmitRef.current = initial;
     return initial;
   });
 
   useEffect(() => {
-    const formStr = JSON.stringify(watched ?? {}, null, 2);
+    const formStr = serialize(watched);
     if (formStr === lastEmitRef.current) return;
     setText(formStr);
     lastEmitRef.current = formStr;
-  }, [watched]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [watched, wrapKey]);
 
   return (
     <JsonEditor
@@ -130,8 +137,11 @@ function AdvancedSliceEditor({
         setText(next);
         try {
           const parsed = JSON.parse(next);
-          form.setFieldValue(path, parsed);
-          lastEmitRef.current = JSON.stringify(parsed, null, 2);
+          const toWrite = wrapKey && parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+            ? (parsed as Record<string, unknown>)[wrapKey] ?? {}
+            : parsed;
+          form.setFieldValue(path, toWrite);
+          lastEmitRef.current = JSON.stringify(wrapKey ? { [wrapKey]: toWrite } : toWrite, null, 2);
         } catch {
           // invalid JSON; keep buffer, don't push to form
         }
@@ -2621,6 +2631,7 @@ export default function InboundFormModal({
                   <AdvancedSliceEditor
                     form={form}
                     path="settings"
+                    wrapKey="settings"
                     minHeight="320px"
                     maxHeight="540px"
                   />
@@ -2640,6 +2651,7 @@ export default function InboundFormModal({
                     <AdvancedSliceEditor
                       form={form}
                       path="streamSettings"
+                      wrapKey="streamSettings"
                       minHeight="320px"
                       maxHeight="540px"
                     />
@@ -2659,6 +2671,7 @@ export default function InboundFormModal({
                   <AdvancedSliceEditor
                     form={form}
                     path="sniffing"
+                    wrapKey="sniffing"
                     minHeight="240px"
                     maxHeight="420px"
                   />