mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 10:00:58 +00:00
feat(frontend): multi-node cloning initial implementation (#6216)
* feat(frontend): multinode cloning initial implementation * fix(frontend): harden live node detection in multinode cloning Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(frontend): add aria label to clone inbound modal * fix(frontend): shallow copy inbound settings during cloning * fix(frontend): avoid potential port conflict during testing * fix(frontend): selection buttons and websocket selection reset fix --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import { Protocols } from '@/schemas/primitives';
|
||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
|
||||
import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
||||
import { NODE_ELIGIBLE_PROTOCOLS } from '@/lib/xray/node-protocols';
|
||||
import { VLESS_AUTH_LABEL_KEYS, vlessEncryptionAuthKind } from '@/lib/xray/vless-encryption';
|
||||
import { SniffingSchema } from '@/schemas/primitives/sniffing';
|
||||
import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
|
||||
@@ -101,14 +102,6 @@ const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label:
|
||||
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
||||
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||
const SHARE_ADDR_HOSTNAME_RE = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
||||
const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
|
||||
Protocols.VLESS,
|
||||
Protocols.VMESS,
|
||||
Protocols.TROJAN,
|
||||
Protocols.SHADOWSOCKS,
|
||||
Protocols.HYSTERIA,
|
||||
Protocols.WIREGUARD,
|
||||
]);
|
||||
|
||||
function isValidShareAddrInput(value: string): boolean {
|
||||
const v = value.trim();
|
||||
@@ -216,7 +209,7 @@ export default function InboundFormModal({
|
||||
|
||||
const selectableNodes = (availableNodes || []).filter((n) => n.enable);
|
||||
const protocol = (useWatch({ control, name: 'protocol' }) ?? '') as string;
|
||||
const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol);
|
||||
const isNodeEligible = !!NODE_ELIGIBLE_PROTOCOLS[protocol];
|
||||
/*
|
||||
* The `node` share-address strategy only means something when the inbound can
|
||||
* actually live on a node — otherwise the node address it would resolve to is
|
||||
@@ -434,7 +427,7 @@ export default function InboundFormModal({
|
||||
const next = getV('protocol') as string;
|
||||
const settings = createDefaultInboundSettings(next) ?? undefined;
|
||||
setV('settings', settings);
|
||||
if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
|
||||
if (!NODE_ELIGIBLE_PROTOCOLS[next]) {
|
||||
setV('nodeId', null);
|
||||
}
|
||||
if (next === Protocols.HYSTERIA) {
|
||||
@@ -534,8 +527,10 @@ export default function InboundFormModal({
|
||||
allowClear
|
||||
options={selectableNodes.map((n) => ({
|
||||
value: n.id,
|
||||
label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
|
||||
disabled: n.status === 'offline',
|
||||
// Same rule as the clone target picker: only online is
|
||||
// deployable (`unknown` = no heartbeat yet).
|
||||
label: `${n.name}${n.status === 'online' ? '' : ` (${n.status || 'offline'})`}`,
|
||||
disabled: n.status !== 'online',
|
||||
}))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
Reference in New Issue
Block a user