Files
3x-ui-agent/frontend/src/pages/xray/balancers/ObservatorySettingsTab.tsx
T
Sanaei f727d04f65
Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

236 lines
7.7 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Empty, Input, InputNumber, Select, Space, Switch, Tag } from 'antd';
import { onNumber } from '@/utils/onNumber';
import { SettingListItem } from '@/components/ui';
import {
BurstObservatorySchema,
ObservatoryHttpMethodSchema,
ObservatorySchema,
type BurstObservatoryObject,
type ObservatoryHttpMethod,
type ObservatoryObject,
type PingConfigObject,
} from '@/schemas/observatory';
import type { XraySettingsValue } from '@/hooks/useXraySetting';
import { settingsRequireBurstObservatory } from './balancer-helpers';
interface ObservatorySettingsTabProps {
templateSettings: XraySettingsValue | null;
mutate: (mutator: (next: XraySettingsValue) => void) => void;
}
const OBSERVATORY_DEFAULTS = ObservatorySchema.parse({});
const BURST_DEFAULTS = BurstObservatorySchema.parse({});
function asObject(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
}
function SelectorTags({ tags }: { tags: string[] }) {
if (!tags || tags.length === 0) return <Tag>—</Tag>;
return (
<>
{tags.map((sel) => (
<Tag
key={sel}
className="info-large-tag"
style={{ margin: 0, marginRight: 4, marginBottom: 4 }}
>
{sel}
</Tag>
))}
</>
);
}
export default function ObservatorySettingsTab({
templateSettings,
mutate,
}: ObservatorySettingsTabProps) {
const { t } = useTranslation();
const observatory = useMemo<ObservatoryObject | null>(() => {
const raw = templateSettings?.observatory;
if (raw == null) return null;
return { ...OBSERVATORY_DEFAULTS, ...asObject(raw) } as ObservatoryObject;
}, [templateSettings?.observatory]);
const burst = useMemo<BurstObservatoryObject | null>(() => {
const raw = templateSettings?.burstObservatory;
if (raw == null) return null;
const merged = { ...BURST_DEFAULTS, ...asObject(raw) } as BurstObservatoryObject;
merged.pingConfig = {
...BURST_DEFAULTS.pingConfig,
...asObject(merged.pingConfig),
} as PingConfigObject;
return merged;
}, [templateSettings?.burstObservatory]);
const hasObservatory = observatory != null;
const hasBurst = burst != null;
const hasMixedObservers = hasObservatory && hasBurst;
const activeView =
hasBurst && (!hasObservatory || settingsRequireBurstObservatory(templateSettings))
? 'burstObservatory'
: 'observatory';
function patchObservatory(patch: Partial<ObservatoryObject>) {
mutate((tt) => {
tt.observatory = { ...OBSERVATORY_DEFAULTS, ...asObject(tt.observatory), ...patch };
});
}
function patchPingConfig(patch: Partial<PingConfigObject>) {
mutate((tt) => {
const current = asObject(tt.burstObservatory);
const currentPing = asObject(current.pingConfig);
tt.burstObservatory = {
...BURST_DEFAULTS,
...current,
pingConfig: { ...BURST_DEFAULTS.pingConfig, ...currentPing, ...patch },
};
});
}
if (!hasObservatory && !hasBurst) {
return <Empty description={t('pages.xray.observatory.emptyHint')} />;
}
const observatorySection = observatory && (
<>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.subjectSelector')}
description={t('pages.xray.observatory.subjectSelectorDesc')}
>
<SelectorTags tags={observatory.subjectSelector} />
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.probeURL')}
description={t('pages.xray.observatory.probeURLDesc')}
>
<Input
value={observatory.probeURL}
onChange={(e) => patchObservatory({ probeURL: e.target.value })}
placeholder="https://www.google.com/generate_204"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.probeInterval')}
description={t('pages.xray.observatory.probeIntervalDesc')}
>
<Input
value={observatory.probeInterval}
onChange={(e) => patchObservatory({ probeInterval: e.target.value })}
placeholder="1m"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.enableConcurrency')}
description={t('pages.xray.observatory.enableConcurrencyDesc')}
>
<Switch
checked={observatory.enableConcurrency}
onChange={(v) => patchObservatory({ enableConcurrency: v })}
/>
</SettingListItem>
</>
);
const burstSection = burst && (
<>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.subjectSelector')}
description={t('pages.xray.observatory.subjectSelectorDesc')}
>
<SelectorTags tags={burst.subjectSelector} />
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.destination')}
description={t('pages.xray.observatory.destinationDesc')}
>
<Input
value={burst.pingConfig.destination}
onChange={(e) => patchPingConfig({ destination: e.target.value })}
placeholder="https://www.google.com/generate_204"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.connectivity')}
description={t('pages.xray.observatory.connectivityDesc')}
>
<Input
value={burst.pingConfig.connectivity}
allowClear
onChange={(e) => patchPingConfig({ connectivity: e.target.value })}
placeholder="http://connectivitycheck.platform.hicloud.com/generate_204"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.interval')}
description={t('pages.xray.observatory.intervalDesc')}
>
<Input
value={burst.pingConfig.interval}
onChange={(e) => patchPingConfig({ interval: e.target.value })}
placeholder="1m"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.timeout')}
description={t('pages.xray.observatory.timeoutDesc')}
>
<Input
value={burst.pingConfig.timeout}
onChange={(e) => patchPingConfig({ timeout: e.target.value })}
placeholder="5s"
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.sampling')}
description={t('pages.xray.observatory.samplingDesc')}
>
<InputNumber
min={1}
value={burst.pingConfig.sampling}
onChange={onNumber((v) => patchPingConfig({ sampling: v }))}
style={{ width: '100%' }}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.xray.observatory.httpMethod')}
description={t('pages.xray.observatory.httpMethodDesc')}
>
<Select<ObservatoryHttpMethod>
value={burst.pingConfig.httpMethod}
onChange={(v) => patchPingConfig({ httpMethod: v })}
options={ObservatoryHttpMethodSchema.options.map((m) => ({ value: m, label: m }))}
style={{ width: '100%' }}
/>
</SettingListItem>
</>
);
return (
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Alert type="info" showIcon title={t('pages.xray.observatory.autoManaged')} />
{hasMixedObservers && (
<Alert type="warning" showIcon title={t('pages.xray.observatory.mixedLegacy')} />
)}
<div>{activeView === 'observatory' ? observatorySection : burstSection}</div>
</Space>
);
}