|
@@ -1,12 +1,21 @@
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
-import { Button, Dropdown, Empty, Modal, Select, Space, Table, Tabs, Tag, Tooltip } from 'antd';
|
|
|
|
|
|
|
+import { Button, Dropdown, Empty, Modal, Select, Space, Table, Tabs, Tag, Tooltip, message } from 'antd';
|
|
|
import { PlusOutlined, MoreOutlined, EditOutlined, DeleteOutlined, SyncOutlined, DeploymentUnitOutlined, RadarChartOutlined } from '@ant-design/icons';
|
|
import { PlusOutlined, MoreOutlined, EditOutlined, DeleteOutlined, SyncOutlined, DeploymentUnitOutlined, RadarChartOutlined } from '@ant-design/icons';
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
|
|
|
import BalancerFormModal from './BalancerFormModal';
|
|
import BalancerFormModal from './BalancerFormModal';
|
|
|
import type { BalancerFormValue } from './BalancerFormModal';
|
|
import type { BalancerFormValue } from './BalancerFormModal';
|
|
|
import { syncObservatories } from './balancer-helpers';
|
|
import { syncObservatories } from './balancer-helpers';
|
|
|
|
|
+import {
|
|
|
|
|
+ isBalancerLoopbackTag,
|
|
|
|
|
+ loopbackTagFor,
|
|
|
|
|
+ resolveLoopbackFallback,
|
|
|
|
|
+ ensureBalancerLoopback,
|
|
|
|
|
+ removeBalancerLoopback,
|
|
|
|
|
+ removeBalancerLoopbackIfOrphaned,
|
|
|
|
|
+ propagateBalancerTagRename,
|
|
|
|
|
+} from './balancer-loopback';
|
|
|
import { planBalancerDeletion, applyBalancerDeletion } from '../reference-cleanup';
|
|
import { planBalancerDeletion, applyBalancerDeletion } from '../reference-cleanup';
|
|
|
import DeletionImpactList from '../DeletionImpactList';
|
|
import DeletionImpactList from '../DeletionImpactList';
|
|
|
import ObservatorySettingsTab from './ObservatorySettingsTab';
|
|
import ObservatorySettingsTab from './ObservatorySettingsTab';
|
|
@@ -44,6 +53,7 @@ interface BalancerRow {
|
|
|
strategy: BalancerStrategyType;
|
|
strategy: BalancerStrategyType;
|
|
|
selector: string[];
|
|
selector: string[];
|
|
|
fallbackTag: string;
|
|
fallbackTag: string;
|
|
|
|
|
+ displayFallbackTag: string;
|
|
|
settings?: BalancerStrategySettings;
|
|
settings?: BalancerStrategySettings;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -63,26 +73,33 @@ export default function BalancersTab({
|
|
|
}: BalancersTabProps) {
|
|
}: BalancersTabProps) {
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
const [modal, modalContextHolder] = Modal.useModal();
|
|
const [modal, modalContextHolder] = Modal.useModal();
|
|
|
|
|
+ const [messageApi, messageContextHolder] = message.useMessage();
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
|
const [editingBalancer, setEditingBalancer] = useState<BalancerFormValue | null>(null);
|
|
const [editingBalancer, setEditingBalancer] = useState<BalancerFormValue | null>(null);
|
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
|
|
|
|
|
|
|
|
+ const balancerObjects = useMemo(
|
|
|
|
|
+ () => (templateSettings?.routing?.balancers || []) as BalancerObject[],
|
|
|
|
|
+ [templateSettings?.routing?.balancers],
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
const rows: BalancerRow[] = useMemo(() => {
|
|
const rows: BalancerRow[] = useMemo(() => {
|
|
|
- const list = (templateSettings?.routing?.balancers || []) as BalancerRecord[];
|
|
|
|
|
|
|
+ const list = balancerObjects;
|
|
|
return list.map((b, idx) => ({
|
|
return list.map((b, idx) => ({
|
|
|
key: idx,
|
|
key: idx,
|
|
|
tag: b.tag || '',
|
|
tag: b.tag || '',
|
|
|
strategy: (b.strategy?.type ?? 'random') as BalancerStrategyType,
|
|
strategy: (b.strategy?.type ?? 'random') as BalancerStrategyType,
|
|
|
selector: b.selector || [],
|
|
selector: b.selector || [],
|
|
|
fallbackTag: b.fallbackTag || '',
|
|
fallbackTag: b.fallbackTag || '',
|
|
|
|
|
+ displayFallbackTag: resolveLoopbackFallback(templateSettings!, b.fallbackTag || ''),
|
|
|
settings: b.strategy?.settings,
|
|
settings: b.strategy?.settings,
|
|
|
}));
|
|
}));
|
|
|
- }, [templateSettings?.routing?.balancers]);
|
|
|
|
|
|
|
+ }, [balancerObjects, templateSettings]);
|
|
|
|
|
|
|
|
const outboundTags = useMemo(() => {
|
|
const outboundTags = useMemo(() => {
|
|
|
const tags = new Set<string>();
|
|
const tags = new Set<string>();
|
|
|
for (const o of templateSettings?.outbounds || []) {
|
|
for (const o of templateSettings?.outbounds || []) {
|
|
|
- if (o?.tag) tags.add(o.tag);
|
|
|
|
|
|
|
+ if (o?.tag && !isBalancerLoopbackTag(o.tag)) tags.add(o.tag);
|
|
|
}
|
|
}
|
|
|
for (const tag of clientReverseTags || []) {
|
|
for (const tag of clientReverseTags || []) {
|
|
|
if (tag) tags.add(tag);
|
|
if (tag) tags.add(tag);
|
|
@@ -98,6 +115,14 @@ export default function BalancersTab({
|
|
|
return rows.filter((b) => b.key !== editingIndex).map((b) => b.tag).filter(Boolean);
|
|
return rows.filter((b) => b.key !== editingIndex).map((b) => b.tag).filter(Boolean);
|
|
|
}, [rows, editingIndex]);
|
|
}, [rows, editingIndex]);
|
|
|
|
|
|
|
|
|
|
+ const balancerTags = useMemo(() => {
|
|
|
|
|
+ return otherTags.filter((tg) => !isBalancerLoopbackTag(tg));
|
|
|
|
|
+ }, [otherTags]);
|
|
|
|
|
+
|
|
|
|
|
+ const overrideOptions: Array<{ value: string; label: React.ReactNode }> = useMemo(() => {
|
|
|
|
|
+ return outboundTags.map((tag) => ({ value: tag, label: tag }));
|
|
|
|
|
+ }, [outboundTags]);
|
|
|
|
|
+
|
|
|
const mutate = useCallback(
|
|
const mutate = useCallback(
|
|
|
(mutator: (next: XraySettingsValue) => void) => {
|
|
(mutator: (next: XraySettingsValue) => void) => {
|
|
|
setTemplateSettings((prev) => {
|
|
setTemplateSettings((prev) => {
|
|
@@ -148,7 +173,12 @@ export default function BalancersTab({
|
|
|
setModalOpen(true);
|
|
setModalOpen(true);
|
|
|
}
|
|
}
|
|
|
function openEdit(idx: number) {
|
|
function openEdit(idx: number) {
|
|
|
- setEditingBalancer(rows[idx]);
|
|
|
|
|
|
|
+ const row = rows[idx];
|
|
|
|
|
+ const resolved: BalancerFormValue = {
|
|
|
|
|
+ ...row,
|
|
|
|
|
+ fallbackTag: resolveLoopbackFallback(templateSettings!, row.fallbackTag),
|
|
|
|
|
+ };
|
|
|
|
|
+ setEditingBalancer(resolved);
|
|
|
setEditingIndex(idx);
|
|
setEditingIndex(idx);
|
|
|
setModalOpen(true);
|
|
setModalOpen(true);
|
|
|
}
|
|
}
|
|
@@ -158,10 +188,11 @@ export default function BalancersTab({
|
|
|
if (!tt.routing) tt.routing = { rules: [], balancers: [] };
|
|
if (!tt.routing) tt.routing = { rules: [], balancers: [] };
|
|
|
if (!Array.isArray(tt.routing.balancers)) tt.routing.balancers = [];
|
|
if (!Array.isArray(tt.routing.balancers)) tt.routing.balancers = [];
|
|
|
const list = tt.routing.balancers as BalancerRecord[];
|
|
const list = tt.routing.balancers as BalancerRecord[];
|
|
|
|
|
+
|
|
|
const wire: BalancerRecord = {
|
|
const wire: BalancerRecord = {
|
|
|
tag: form.tag,
|
|
tag: form.tag,
|
|
|
selector: [...form.selector],
|
|
selector: [...form.selector],
|
|
|
- fallbackTag: form.fallbackTag || '',
|
|
|
|
|
|
|
+ fallbackTag: '',
|
|
|
};
|
|
};
|
|
|
if (form.strategy && form.strategy !== 'random') {
|
|
if (form.strategy && form.strategy !== 'random') {
|
|
|
wire.strategy = { type: form.strategy };
|
|
wire.strategy = { type: form.strategy };
|
|
@@ -169,16 +200,42 @@ export default function BalancersTab({
|
|
|
wire.strategy.settings = form.settings;
|
|
wire.strategy.settings = form.settings;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const isFallbackABalancer = form.fallbackTag && balancerTags.includes(form.fallbackTag);
|
|
|
|
|
+
|
|
|
|
|
+ if (isFallbackABalancer) {
|
|
|
|
|
+ wire.fallbackTag = loopbackTagFor(form.fallbackTag);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ wire.fallbackTag = form.fallbackTag || '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (editingIndex == null) {
|
|
if (editingIndex == null) {
|
|
|
list.push(wire);
|
|
list.push(wire);
|
|
|
|
|
+ if (isFallbackABalancer) {
|
|
|
|
|
+ ensureBalancerLoopback(tt, form.fallbackTag);
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
const oldTag = list[editingIndex]?.tag;
|
|
const oldTag = list[editingIndex]?.tag;
|
|
|
|
|
+ const oldFallback = list[editingIndex]?.fallbackTag || '';
|
|
|
list[editingIndex] = wire;
|
|
list[editingIndex] = wire;
|
|
|
|
|
+
|
|
|
if (oldTag && oldTag !== wire.tag) {
|
|
if (oldTag && oldTag !== wire.tag) {
|
|
|
const rules = tt.routing.rules || [];
|
|
const rules = tt.routing.rules || [];
|
|
|
for (const rule of rules) {
|
|
for (const rule of rules) {
|
|
|
if (rule?.balancerTag === oldTag) rule.balancerTag = wire.tag;
|
|
if (rule?.balancerTag === oldTag) rule.balancerTag = wire.tag;
|
|
|
}
|
|
}
|
|
|
|
|
+ propagateBalancerTagRename(tt, oldTag, wire.tag);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const oldTarget = isBalancerLoopbackTag(oldFallback)
|
|
|
|
|
+ ? (oldFallback.slice(4))
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ if (oldTarget && oldTarget !== form.fallbackTag) {
|
|
|
|
|
+ removeBalancerLoopbackIfOrphaned(tt, oldTarget);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isFallbackABalancer) {
|
|
|
|
|
+ ensureBalancerLoopback(tt, form.fallbackTag);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
syncObservatories(tt);
|
|
syncObservatories(tt);
|
|
@@ -187,6 +244,15 @@ export default function BalancersTab({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function confirmDelete(idx: number) {
|
|
function confirmDelete(idx: number) {
|
|
|
|
|
+ const deletedTag = rows[idx]?.tag;
|
|
|
|
|
+ const lbTag = loopbackTagFor(deletedTag);
|
|
|
|
|
+ const dependents = (templateSettings?.routing?.balancers || [])
|
|
|
|
|
+ .filter((b) => b.tag !== deletedTag && b.fallbackTag === lbTag)
|
|
|
|
|
+ .map((b) => b.tag);
|
|
|
|
|
+ if (dependents.length > 0) {
|
|
|
|
|
+ messageApi.error(t('pages.xray.balancer.balancerDeleteInUse', { names: dependents.join(', ') }));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const impact = templateSettings
|
|
const impact = templateSettings
|
|
|
? planBalancerDeletion(templateSettings, idx)
|
|
? planBalancerDeletion(templateSettings, idx)
|
|
|
: { rules: [], balancers: [], observatory: false, burst: false };
|
|
: { rules: [], balancers: [], observatory: false, burst: false };
|
|
@@ -196,7 +262,11 @@ export default function BalancersTab({
|
|
|
okText: t('delete'),
|
|
okText: t('delete'),
|
|
|
okType: 'danger',
|
|
okType: 'danger',
|
|
|
cancelText: t('cancel'),
|
|
cancelText: t('cancel'),
|
|
|
- onOk: () => mutate((tt) => applyBalancerDeletion(tt, idx)),
|
|
|
|
|
|
|
+ onOk: () => mutate((tt) => {
|
|
|
|
|
+ const tag = tt.routing?.balancers?.[idx]?.tag ?? '';
|
|
|
|
|
+ removeBalancerLoopback(tt, tag);
|
|
|
|
|
+ applyBalancerDeletion(tt, idx);
|
|
|
|
|
+ }),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -272,7 +342,7 @@ export default function BalancersTab({
|
|
|
</Tag>
|
|
</Tag>
|
|
|
)),
|
|
)),
|
|
|
},
|
|
},
|
|
|
- { title: 'Fallback', dataIndex: 'fallbackTag', key: 'fallbackTag', align: 'center', width: 160 },
|
|
|
|
|
|
|
+ { title: 'Fallback', dataIndex: 'displayFallbackTag', key: 'displayFallbackTag', align: 'center', width: 160 },
|
|
|
{
|
|
{
|
|
|
title: t('pages.xray.balancerLive'),
|
|
title: t('pages.xray.balancerLive'),
|
|
|
key: 'live',
|
|
key: 'live',
|
|
@@ -287,9 +357,13 @@ export default function BalancersTab({
|
|
|
</Tooltip>
|
|
</Tooltip>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
- const picked = live.override || live.selected?.[0] || record.fallbackTag;
|
|
|
|
|
|
|
+ const resolve = (tag: string) => isBalancerLoopbackTag(tag) ? resolveLoopbackFallback(templateSettings!, tag) : tag;
|
|
|
|
|
+ const picked = live.override ? resolve(live.override) : live.selected?.[0] ? resolve(live.selected[0]) : record.displayFallbackTag;
|
|
|
|
|
+ const tooltipText = live.override
|
|
|
|
|
+ ? resolve(live.override)
|
|
|
|
|
+ : (live.selected || []).map(resolve).join(', ');
|
|
|
return (
|
|
return (
|
|
|
- <Tooltip title={(live.selected || []).join(', ') || undefined}>
|
|
|
|
|
|
|
+ <Tooltip title={tooltipText || undefined}>
|
|
|
<Tag color={live.override ? 'orange' : 'blue'}>{picked || '—'}</Tag>
|
|
<Tag color={live.override ? 'orange' : 'blue'}>{picked || '—'}</Tag>
|
|
|
</Tooltip>
|
|
</Tooltip>
|
|
|
);
|
|
);
|
|
@@ -302,6 +376,23 @@ export default function BalancersTab({
|
|
|
width: 200,
|
|
width: 200,
|
|
|
render: (_v, record) => {
|
|
render: (_v, record) => {
|
|
|
const live = liveStatus[record.tag];
|
|
const live = liveStatus[record.tag];
|
|
|
|
|
+ const resolvedFB = record.displayFallbackTag;
|
|
|
|
|
+ let options = overrideOptions;
|
|
|
|
|
+ if (resolvedFB && !outboundTags.includes(resolvedFB)) {
|
|
|
|
|
+ options = [...overrideOptions, {
|
|
|
|
|
+ value: resolvedFB,
|
|
|
|
|
+ label: (
|
|
|
|
|
+ <span>
|
|
|
|
|
+ <Tag color="blue" style={{ marginRight: 4 }}>{t('pages.xray.rules.balancer')}</Tag>
|
|
|
|
|
+ {resolvedFB}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ }];
|
|
|
|
|
+ }
|
|
|
|
|
+ const rawOverride = live?.override || undefined;
|
|
|
|
|
+ const resolvedOverride = rawOverride && isBalancerLoopbackTag(rawOverride)
|
|
|
|
|
+ ? resolveLoopbackFallback(templateSettings!, rawOverride)
|
|
|
|
|
+ : rawOverride;
|
|
|
return (
|
|
return (
|
|
|
<Select
|
|
<Select
|
|
|
size="small"
|
|
size="small"
|
|
@@ -309,8 +400,8 @@ export default function BalancersTab({
|
|
|
placeholder={t('pages.xray.balancerOverridePh')}
|
|
placeholder={t('pages.xray.balancerOverridePh')}
|
|
|
allowClear
|
|
allowClear
|
|
|
disabled={!live?.running}
|
|
disabled={!live?.running}
|
|
|
- value={live?.override || undefined}
|
|
|
|
|
- options={outboundTags.map((tag) => ({ label: tag, value: tag }))}
|
|
|
|
|
|
|
+ value={resolvedOverride}
|
|
|
|
|
+ options={options}
|
|
|
onChange={(v) => setOverride(record.tag, (v as string | undefined) || '')}
|
|
onChange={(v) => setOverride(record.tag, (v as string | undefined) || '')}
|
|
|
/>
|
|
/>
|
|
|
);
|
|
);
|
|
@@ -353,6 +444,7 @@ export default function BalancersTab({
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
|
{modalContextHolder}
|
|
{modalContextHolder}
|
|
|
|
|
+ {messageContextHolder}
|
|
|
<Tabs
|
|
<Tabs
|
|
|
items={[
|
|
items={[
|
|
|
{
|
|
{
|
|
@@ -378,6 +470,9 @@ export default function BalancersTab({
|
|
|
open={modalOpen}
|
|
open={modalOpen}
|
|
|
balancer={editingBalancer}
|
|
balancer={editingBalancer}
|
|
|
outboundTags={outboundTags}
|
|
outboundTags={outboundTags}
|
|
|
|
|
+ balancerTags={balancerTags}
|
|
|
|
|
+ balancers={balancerObjects}
|
|
|
|
|
+ templateSettings={templateSettings}
|
|
|
otherTags={otherTags}
|
|
otherTags={otherTags}
|
|
|
onClose={() => setModalOpen(false)}
|
|
onClose={() => setModalOpen(false)}
|
|
|
onConfirm={onConfirm}
|
|
onConfirm={onConfirm}
|