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.
This commit is contained in:
MHSanaei
2026-05-26 16:08:52 +02:00
parent 60350f93e7
commit 36afdf53af
2 changed files with 77 additions and 52 deletions

View File

@@ -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>
);
}

View File

@@ -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"
/>