import { CircleHelp, Settings2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import type { WAFIPGroup, WAFRuleNode } from '@/lib/services/openflare';
import { countryOptions, regionOptions, type GeoOption } from './geo-options';
import { NODE_TYPE_LABELS } from './node-factory';
import { UA_BROWSER_OPTIONS, UA_OS_OPTIONS } from './ua-options';
export function NodeProperties({
node,
ipGroups,
onChange,
}: {
node: WAFRuleNode;
ipGroups: WAFIPGroup[];
onChange: (node: WAFRuleNode) => void;
}) {
const t = useTranslations('waf.editor');
return (
);
}
function PropertyFields({
node,
ipGroups,
onChange,
}: {
node: WAFRuleNode;
ipGroups: WAFIPGroup[];
onChange: (node: WAFRuleNode) => void;
}) {
const t = useTranslations('waf.editor');
if (node.type === 'start' || node.type === 'allow')
return (
{t('systemNodeNoConfig')}
);
if (node.type === 'ip_match')
return (
onChange({ ...node, config: { ...node.config, ips } })
}
/>
onChange({ ...node, config: { ...node.config, cidrs } })
}
/>
({
value: String(group.id),
label: group.name,
searchText: `${group.name} ${group.id}`,
}))}
value={node.config.ip_group_ids.map(String)}
onChange={(values) =>
onChange({
...node,
config: { ...node.config, ip_group_ids: values.map(Number) },
})
}
/>
);
if (node.type === 'geo_match')
return (
onChange({ ...node, config: { ...node.config, countries } })
}
/>
onChange({ ...node, config: { ...node.config, regions } })
}
/>
);
if (node.type === 'ua_check')
return (
{t('uaCheck')}
{t('enableUaCheck')}
onChange({ ...node, config: { ...node.config, require_ua } })
}
/>
{node.config.require_ua && (
<>
{t('block')}
{t('blockBots')}
onChange({
...node,
config: { ...node.config, block_common_bots },
})
}
/>
{t('blockAbnormal')}
onChange({
...node,
config: { ...node.config, block_abnormal_ua },
})
}
/>
{t('blockCustom')}
onChange({
...node,
config: { ...node.config, block_custom_ua },
})
}
/>
{node.config.block_custom_ua && (
{t('customUaPatterns')}
)}
{t('uaMatch')}
{t('matchMode')}
({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.browsers}
onChange={(browsers) =>
onChange({ ...node, config: { ...node.config, browsers } })
}
/>
({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.operating_systems}
onChange={(operating_systems) =>
onChange({
...node,
config: { ...node.config, operating_systems },
})
}
/>
>
)}
);
if (node.type === 'security_check')
return (
{t('basicProtection')}
{(
[
'path_traversal',
'file_inclusion',
'sql_injection',
'command_injection',
'xss',
'ssrf',
'malicious_upload',
'xxe',
'crlf_injection',
] as const
).map((key) => (
{t(`securityRules.${key}.label`)}
onChange({
...node,
config: { ...node.config, [key]: checked },
})
}
/>
))}
);
if (node.type === 'pow')
return (
{t('algorithm')}
{(['difficulty', 'session_ttl', 'challenge_ttl'] as const).map(
(key) => (
onChange({ ...node, config: { ...node.config, [key]: value } })
}
/>
),
)}
);
return (
onChange({ ...node, config: { ...node.config, status_code } })
}
/>
{t('responseBody')}
);
}
function FieldHelp({ tip }: { tip: string }) {
const t = useTranslations('waf.editor');
return (
{tip}
);
}
function DisplayNameField({
node,
onChange,
}: {
node: WAFRuleNode;
onChange: (node: WAFRuleNode) => void;
}) {
const t = useTranslations('waf.editor');
return (
{t('displayName')}
onChange({ ...node, label: event.target.value })}
/>
);
}
function CsvField({
id,
label,
value,
onChange,
}: {
id: string;
label: string;
value: string[];
onChange: (value: string[]) => void;
}) {
const t = useTranslations('waf.editor');
return (
{label}
);
}
function NumberField({
id,
label,
value,
min,
max,
onChange,
}: {
id: string;
label: string;
value: number;
min?: number;
max?: number;
onChange: (value: number) => void;
}) {
return (
{label}
onChange(Number(event.target.value))}
/>
);
}
function MultiSelect({
id,
label,
description,
options,
value,
creatablePattern,
searchRequired = false,
onChange,
}: {
id: string;
label: string;
description?: string;
options: GeoOption[];
value: string[];
creatablePattern?: RegExp;
searchRequired?: boolean;
onChange: (value: string[]) => void;
}) {
const t = useTranslations('waf.editor');
const [draft, setDraft] = useState('');
const normalized = draft.trim().toUpperCase();
const query = draft.trim().toLocaleLowerCase();
const available = [
...options,
...value
.filter(
(selected) => !options.some((option) => option.value === selected),
)
.map((selected) => ({
value: selected,
label: selected,
searchText: selected,
})),
];
const visible = available.filter((option) => {
if (!query) return !searchRequired || value.includes(option.value);
return option.searchText.toLocaleLowerCase().includes(query);
});
const canCreate = Boolean(
creatablePattern?.test(normalized) && !value.includes(normalized),
);
return (
{label}
{(creatablePattern || options.length > 0) && (
setDraft(event.target.value)}
/>
{creatablePattern && (
)}
)}
{visible.length === 0 ? (
{searchRequired && !query
? t('searchOptions', { count: options.length })
: t('noMatchingOptions')}
) : (
visible.map((option) => (
))
)}
{description && {description}}
);
}